/*-------------------------------------------------------------------------
 *
 * verify_nbtree.c
 *		验证基于不变性的 nbtree 索引的完整性。
 *
 * 对于 B-Tree 索引，验证包括检查目标索引中的每一页
 * 按照插入扫描键报告的逻辑顺序有条目
 * （验证需要插入扫描键的排序 NULL 语义）。
 *
 * 当请求索引到堆的验证时，使用布隆过滤器来
 * 指纹化目标索引中的所有元组，因为索引在
 * 验证其结构时被遍历。一个堆扫描稍后使用布隆过滤器探针来验证
 * 每个可见的堆元组是否有匹配的索引元组。
 *
 *
 * Copyright (c) 2017-2022, PostgreSQL Global Development Group
 *
 * IDENTIFICATION
 *	  contrib/amcheck/verify_nbtree.c
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include "access/heaptoast.h"
#include "access/htup_details.h"
#include "access/nbtree.h"
#include "access/table.h"
#include "access/tableam.h"
#include "access/transam.h"
#include "access/xact.h"
#include "catalog/index.h"
#include "catalog/pg_am.h"
#include "catalog/pg_opfamily_d.h"
#include "commands/tablecmds.h"
#include "common/pg_prng.h"
#include "lib/bloomfilter.h"
#include "miscadmin.h"
#include "storage/lmgr.h"
#include "storage/smgr.h"
#include "utils/memutils.h"
#include "utils/snapmgr.h"


PG_MODULE_MAGIC;

/*
 * B-树不可能有这么多层，因为每层必须有一个块，这受到BlockNumber范围的限制：
 */
#define InvalidBtreeLevel	((uint32) InvalidBlockNumber)
#define BTreeTupleGetNKeyAtts(itup, rel)   \
	Min(IndexRelationGetNumberOfKeyAttributes(rel), BTreeTupleGetNAtts(itup, rel))

/*
 * 与验证B-树索引相关的状态
 *
 * target是验证操作的参考点。
 *
 * 其他B-树页面可能已分配，但这些页面始终是辅助页面（例如，它们是当前target的子页面）。从概念上讲，问题仅在当前target页面中发现（或者在heapallindexed验证期间针对特定的heap元组）。通过验证的左右、上下扫描找到的每个页面恰好变成目标一次。
 */
typedef struct BtreeCheckState
{
	/*
	 * 不变状态，在验证开始时建立：
	 */

	/* B-树索引关系和相关的heap关系 */
	Relation	rel;
	Relation	heaprel;
	/* rel是heapkeyspace索引吗？ */
	bool		heapkeyspace;
	/* 对heap/index持有ShareLock，而不是AccessShareLock？ */
	bool		readonly;
	/* 还在验证heap没有未建立索引的元组？ */
	bool		heapallindexed;
	/* 还确保非支点元组可以通过新的搜索找到？ */
	bool		rootdescend;
	/* 每页上下文 */
	MemoryContext targetcontext;
	/* 缓冲区访问策略 */
	BufferAccessStrategy checkstrategy;

	/*
	 * 可变状态，用于验证特定页面：
	 */

	/* 当前目标页面 */
	Page		target;
	/* 目标块编号 */
	BlockNumber targetblock;
	/* 目标页面的LSN */
	XLogRecPtr	targetlsn;

	/*
	 * 低键：目标页面左兄弟的高键。仅用于子页面验证。因此，只有在设置了'readonly'时，'lowkey'才会被保留。
	 */
	IndexTuple	lowkey;

	/*
	 * 从目标页面通过下链接访问的下一级块的右链接和不完整拆分标志。我们用它来检查缺失的下链接。
	 */
	BlockNumber prevrightlink;
	bool		previncompletesplit;

	/*
	 * 可变状态，用于可选的heapallindexed验证：
	 */

	/* Bloom过滤器指纹B-树索引 */
	bloom_filter *filter;
	/* 调试计数器 */
	int64		heaptuplespresent;
} BtreeCheckState;

/*
 * 验证整个B-树索引级别的起始点
 */
typedef struct BtreeLevel
{
	/* 级别编号（0是叶子页面级别）。 */
	uint32		level;

	/* 该级别左侧最左边的块。级别的扫描从这里开始。 */
	BlockNumber leftmost;

	/* 这个级别是否被元页面报告为“真实”的根级别？ */
	bool		istruerootlevel;
} BtreeLevel;

PG_FUNCTION_INFO_V1(bt_index_check);
PG_FUNCTION_INFO_V1(bt_index_parent_check);

static void fc_bt_index_check_internal(Oid fc_indrelid, bool fc_parentcheck,
									bool fc_heapallindexed, bool fc_rootdescend);
static inline void fc_btree_index_checkable(Relation fc_rel);
static inline bool fc_btree_index_mainfork_expected(Relation fc_rel);
static void fc_bt_check_every_level(Relation fc_rel, Relation fc_heaprel,
								 bool fc_heapkeyspace, bool fc_readonly, bool fc_heapallindexed,
								 bool fc_rootdescend);
static BtreeLevel fc_bt_check_level_from_leftmost(BtreeCheckState *fc_state,
											   BtreeLevel fc_level);
static bool fc_bt_leftmost_ignoring_half_dead(BtreeCheckState *fc_state,
										   BlockNumber fc_start,
										   BTPageOpaque fc_start_opaque);
static void fc_bt_recheck_sibling_links(BtreeCheckState *fc_state,
									 BlockNumber fc_btpo_prev_from_target,
									 BlockNumber fc_leftcurrent);
static void fc_bt_target_page_check(BtreeCheckState *fc_state);
static BTScanInsert fc_bt_right_page_check_scankey(BtreeCheckState *fc_state);
static void fc_bt_child_check(BtreeCheckState *fc_state, BTScanInsert fc_targetkey,
						   OffsetNumber fc_downlinkoffnum);
static void fc_bt_child_highkey_check(BtreeCheckState *fc_state,
								   OffsetNumber fc_target_downlinkoffnum,
								   Page fc_loaded_child,
								   uint32 fc_target_level);
static void fc_bt_downlink_missing_check(BtreeCheckState *fc_state, bool fc_rightsplit,
									  BlockNumber fc_targetblock, Page fc_target);
static void fc_bt_tuple_present_callback(Relation fc_index, ItemPointer fc_tid,
									  Datum *fc_values, bool *fc_isnull,
									  bool fc_tupleIsAlive, void *fc_checkstate);
static IndexTuple fc_bt_normalize_tuple(BtreeCheckState *fc_state,
									 IndexTuple fc_itup);
static inline IndexTuple fc_bt_posting_plain_tuple(IndexTuple fc_itup, int fc_n);
static bool fc_bt_rootdescend(BtreeCheckState *fc_state, IndexTuple fc_itup);
static inline bool fc_offset_is_negative_infinity(BTPageOpaque fc_opaque,
											   OffsetNumber fc_offset);
static inline bool fc_invariant_l_offset(BtreeCheckState *fc_state, BTScanInsert fc_key,
									  OffsetNumber fc_upperbound);
static inline bool fc_invariant_leq_offset(BtreeCheckState *fc_state,
										BTScanInsert fc_key,
										OffsetNumber fc_upperbound);
static inline bool fc_invariant_g_offset(BtreeCheckState *fc_state, BTScanInsert fc_key,
									  OffsetNumber fc_lowerbound);
static inline bool fc_invariant_l_nontarget_offset(BtreeCheckState *fc_state,
												BTScanInsert fc_key,
												BlockNumber fc_nontargetblock,
												Page fc_nontarget,
												OffsetNumber fc_upperbound);
static Page fc_palloc_btree_page(BtreeCheckState *fc_state, BlockNumber fc_blocknum);
static inline BTScanInsert fc_bt_mkscankey_pivotsearch(Relation fc_rel,
													IndexTuple fc_itup);
static ItemId fc_PageGetItemIdCareful(BtreeCheckState *fc_state, BlockNumber fc_block,
								   Page fc_page, OffsetNumber fc_offset);
static inline ItemPointer fc_BTreeTupleGetHeapTIDCareful(BtreeCheckState *fc_state,
													  IndexTuple fc_itup, bool fc_nonpivot);
static inline ItemPointer fc_BTreeTupleGetPointsToTID(IndexTuple fc_itup);

/*
 * bt_index_check(index regclass, heapallindexed boolean)
 *
 * 验证B-树索引的完整性。
 *
 * 对heap和索引关系获得AccessShareLock。不会考虑父/子页面之间存在的不变性。可选地验证heap中不包含任何未建立索引或错误索引的元组。
 */
Datum bt_index_check(PG_FUNCTION_ARGS)
{
	Oid			fc_indrelid = PG_GETARG_OID(0);
	bool		fc_heapallindexed = false;

	if (PG_NARGS() == 2)
		fc_heapallindexed = PG_GETARG_BOOL(1);

	fc_bt_index_check_internal(fc_indrelid, false, fc_heapallindexed, false);

	PG_RETURN_VOID();
}

/*
 * bt_index_parent_check(index regclass, heapallindexed boolean)
 *
 * 验证B-树索引的完整性。
 *
 * 对heap和索引关系获得ShareLock。验证父页面中的下链接是子页面的有效下界。可选地验证heap中不包含任何未建立索引或错误索引的元组。
 */
Datum bt_index_parent_check(PG_FUNCTION_ARGS)
{
	Oid			fc_indrelid = PG_GETARG_OID(0);
	bool		fc_heapallindexed = false;
	bool		fc_rootdescend = false;

	if (PG_NARGS() >= 2)
		fc_heapallindexed = PG_GETARG_BOOL(1);
	if (PG_NARGS() == 3)
		fc_rootdescend = PG_GETARG_BOOL(2);

	fc_bt_index_check_internal(fc_indrelid, true, fc_heapallindexed, fc_rootdescend);

	PG_RETURN_VOID();
}

/*
 * bt_index_[parent_]check的助手，协调大部分工作。
 */
static void fc_bt_index_check_internal(Oid fc_indrelid, bool fc_parentcheck, bool fc_heapallindexed,
						bool fc_rootdescend)
{
	Oid			fc_heapid;
	Relation	fc_indrel;
	Relation	fc_heaprel;
	LOCKMODE	fc_lockmode;
	Oid			fc_save_userid;
	int			fc_save_sec_context;
	int			fc_save_nestlevel;

	if (fc_parentcheck)
		fc_lockmode = ShareLock;
	else
		fc_lockmode = AccessShareLock;

	/*
	 * 我们必须在索引之前锁定表以避免死锁。然而，如果
	 * 传递的 indrelid 不是索引，则 IndexGetRelation() 将失败。
	 * 与其发出不太有用的错误消息，不如推迟
	 * 抱怨，期待下面的 is-it-an-index 测试将失败。
	 *
	 * 在热备份模式下，当 parentcheck 为 true 时，这将引发错误。
	 */
	fc_heapid = IndexGetRelation(fc_indrelid, true);
	if (OidIsValid(fc_heapid))
	{
		fc_heaprel = table_open(fc_heapid, fc_lockmode);

		/*
		 * 切换到表所有者的用户 ID，以便任何索引函数都
		 * 作为该用户运行。同时锁定受限的安全操作
		 * 并安排使 GUC 变量更改对该命令本地化。
		 */
		GetUserIdAndSecContext(&fc_save_userid, &fc_save_sec_context);
		SetUserIdAndSecContext(fc_heaprel->rd_rel->relowner,
							   fc_save_sec_context | SECURITY_RESTRICTED_OPERATION);
		fc_save_nestlevel = NewGUCNestLevel();
	}
	else
	{
		fc_heaprel = NULL;
		/* 只需设置这些以抑制“未初始化变量”警告 */
		fc_save_userid = InvalidOid;
		fc_save_sec_context = -1;
		fc_save_nestlevel = -1;
	}

	/*
	 * 单独打开目标索引关系（像 relation_openrv()，但
	 * 首先锁定堆关系以防止死锁）。在热备份
	 * 模式下，当 parentcheck 为 true 时，这将引发错误。
	 *
	 * 即使在 heapallindexed 的情况下，这里也不需要通常的 indcheckxmin 可用性测试，
	 * 因为正在验证的索引只需有新事务快照的条目。
	 * （如果这是一个 parentcheck 验证，则没有问题关于
	 * 已提交或最近死去的堆元组由于
	 * 并发活动而缺少索引条目。）
	 */
	fc_indrel = index_open(fc_indrelid, fc_lockmode);

	/*
	 * 由于我们在上面没有任何锁的情况下进行了 IndexGetRelation 调用，
	 * 因此几乎可能存在与索引删除/重建的竞态
	 * 导致我们获取了错误的表。
	 */
	if (fc_heaprel == NULL || fc_heapid != IndexGetRelation(fc_indrelid, false))
		ereport(ERROR,
				(errcode(ERRCODE_UNDEFINED_TABLE),
				 errmsg("could not open parent table of index \"%s\"",
						RelationGetRelationName(fc_indrel))));

	/* 适合检查为 B-Tree 的关系？ */
	fc_btree_index_checkable(fc_indrel);

	if (fc_btree_index_mainfork_expected(fc_indrel))
	{
		bool		fc_heapkeyspace,
					fc_allequalimage;

		if (!smgrexists(RelationGetSmgr(fc_indrel), MAIN_FORKNUM))
			ereport(ERROR,
					(errcode(ERRCODE_INDEX_CORRUPTED),
					 errmsg("index \"%s\" lacks a main relation fork",
							RelationGetRelationName(fc_indrel))));

		/* 从元页面提取元数据，并在传递中进行清理 */
		_bt_metaversion(fc_indrel, &fc_heapkeyspace, &fc_allequalimage);
		if (fc_allequalimage && !fc_heapkeyspace)
			ereport(ERROR,
					(errcode(ERRCODE_INDEX_CORRUPTED),
					 errmsg("index \"%s\" metapage has equalimage field set on unsupported nbtree version",
							RelationGetRelationName(fc_indrel))));
		if (fc_allequalimage && !_bt_allequalimage(fc_indrel, false))
		{
			bool		fc_has_interval_ops = false;

			for (int fc_i = 0; fc_i < IndexRelationGetNumberOfKeyAttributes(fc_indrel); fc_i++)
				if (fc_indrel->rd_opfamily[fc_i] == INTERVAL_BTREE_FAM_OID)
					fc_has_interval_ops = true;
			ereport(ERROR,
					(errcode(ERRCODE_INDEX_CORRUPTED),
					 errmsg("index \"%s\" metapage incorrectly indicates that deduplication is safe",
							RelationGetRelationName(fc_indrel)),
					 fc_has_interval_ops
					 ? errhint("This is known of \"interval\" indexes last built on a version predating 2023-11.")
					 : 0));
		}

		/* 检查索引，可能与其所在表进行对比 */
		fc_bt_check_every_level(fc_indrel, fc_heaprel, fc_heapkeyspace, fc_parentcheck,
							 fc_heapallindexed, fc_rootdescend);
	}

	/* 回滚索引函数执行的任何 GUC 更改 */
	AtEOXact_GUC(false, fc_save_nestlevel);

	/* 恢复 userid 和安全上下文 */
	SetUserIdAndSecContext(fc_save_userid, fc_save_sec_context);

	/*
	 * 及早释放锁。这在这里是可以的，因为被调用
	 * 的例程中没有任何东西会触发共享缓存无效通知被发送，
	 * 因此我们可以放宽通常仅在提交后释放锁的模式。
	 */
	index_close(fc_indrel, fc_lockmode);
	if (fc_heaprel)
		table_close(fc_heaprel, fc_lockmode);
}

/*
 * 关于关系作为 B-Tree 索引检查的适用性基本检查。
 *
 * 注意：故意不检查权限，该函数通常不可由非超级用户调用。
 * 如果被授予，则能够检查整个集群是有用的。
 */
static inline void fc_btree_index_checkable(Relation fc_rel)
{
	if (fc_rel->rd_rel->relkind != RELKIND_INDEX ||
		fc_rel->rd_rel->relam != BTREE_AM_OID)
		ereport(ERROR,
				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
				 errmsg("only B-Tree indexes are supported as targets for verification"),
				 errdetail("Relation \"%s\" is not a B-Tree index.",
						   RelationGetRelationName(fc_rel))));

	if (RELATION_IS_OTHER_TEMP(fc_rel))
		ereport(ERROR,
				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
				 errmsg("cannot access temporary tables of other sessions"),
				 errdetail("Index \"%s\" is associated with temporary relation.",
						   RelationGetRelationName(fc_rel))));

	if (!fc_rel->rd_index->indisvalid)
		ereport(ERROR,
				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
				 errmsg("cannot check index \"%s\"",
						RelationGetRelationName(fc_rel)),
				 errdetail("Index is not valid.")));
}

/*
 * 检查 B-Tree 索引关系是否应该为其主关系
 * 叉具有文件。验证使用此来跳过未记录的索引
 * 当处于热备份模式时，在这种模式下，根本没有任何内容可以验证。
 * 我们表现得好像该关系是空的。
 *
 * 注意：调用者应在此处调用 btree_index_checkable() 之前调用。
 */
static inline bool fc_btree_index_mainfork_expected(Relation fc_rel)
{
	if (fc_rel->rd_rel->relpersistence != RELPERSISTENCE_UNLOGGED ||
		!RecoveryInProgress())
		return true;

	ereport(DEBUG1,
			(errcode(ERRCODE_READ_ONLY_SQL_TRANSACTION),
			 errmsg("cannot verify unlogged index \"%s\" during recovery, skipping",
					RelationGetRelationName(fc_rel))));

	return false;
}

/*
 * B-树 SQL 可调用函数的主入口点。按照逻辑顺序遍历 B-树，边走边验证不变性。可选地，验证检查堆关系是否包含任何在索引中未表示但应该存在的元组。
 *
 * 调用者有责任在索引关系上获取适当的重量级锁，并在持有 ShareLock 时通知我们额外检查是否安全。 （相同类型的锁也必须在堆关系上获得。）
 *
 * 通常假定 ShareLock 防止对索引结构的任何物理修改，包括 VACUUM 可能进行的修改。这不包括并发索引扫描设置的 LP_DEAD 位，尽管那只是元数据，无法直接影响此处执行的任何检查。任何可能对设置的 LP_DEAD 位（回收空间）采取行动的并发进程需要一个重量级锁，而在我们持有 ShareLock 时无法持有该锁。（此外，即使那种情况可能发生，当页面可能会分裂时的临时回收是逐页进行的，并且需要一个独占的缓冲区锁，这不会给我们带来麻烦。_bt_delitems_vacuum() 只能删除叶子项，因此额外的父/子检查不会受到影响。）
 */
static void fc_bt_check_every_level(Relation fc_rel, Relation fc_heaprel, bool fc_heapkeyspace,
					 bool fc_readonly, bool fc_heapallindexed, bool fc_rootdescend)
{
	BtreeCheckState *fc_state;
	Page		fc_metapage;
	BTMetaPageData *fc_metad;
	uint32		fc_previouslevel;
	BtreeLevel	fc_current;
	Snapshot	fc_snapshot = SnapshotAny;

	if (!fc_readonly)
		elog(DEBUG1, "verifying consistency of tree structure for index \"%s\"",
			 RelationGetRelationName(fc_rel));
	else
		elog(DEBUG1, "verifying consistency of tree structure for index \"%s\" with cross-level checks",
			 RelationGetRelationName(fc_rel));

	/*
	 * 此断言与 index_getnext_tid() 中的断言匹配。有关页面回收/“对所有人可见”注释，请参见 nbtree README。
	 */
	Assert(TransactionIdIsValid(RecentXmin));

	/*
	 * 为整个验证操作初始化状态
	 */
	fc_state = palloc0(sizeof(BtreeCheckState));
	fc_state->rel = fc_rel;
	fc_state->heaprel = fc_heaprel;
	fc_state->heapkeyspace = fc_heapkeyspace;
	fc_state->readonly = fc_readonly;
	fc_state->heapallindexed = fc_heapallindexed;
	fc_state->rootdescend = fc_rootdescend;

	if (fc_state->heapallindexed)
	{
		int64		fc_total_pages;
		int64		fc_total_elems;
		uint64		fc_seed;

		/*
		 * 根据索引中估计的元组数量调整 Bloom 过滤器的大小，同时保守地假设每个块必须至少包含 MaxTIDsPerBTreePage / 3 个“普通”元组 -- 参见
		 * bt_posting_plain_tuple() 的定义，以及处理发布列表元组的细节。
		 */
		fc_total_pages = RelationGetNumberOfBlocks(fc_rel);
		fc_total_elems = Max(fc_total_pages * (MaxTIDsPerBTreePage / 3),
						  (int64) fc_state->rel->rd_rel->reltuples);
		/* 生成随机种子以避免重复 */
		fc_seed = pg_prng_uint64(&pg_global_prng_state);
		/* 创建 Bloom 过滤器以指纹索引 */
		fc_state->filter = bloom_create(fc_total_elems, maintenance_work_mem, fc_seed);
		fc_state->heaptuplespresent = 0;

		/*
		 * 在非只读情况下注册我们自己的快照，而不是让 table_index_build_scan() 在稍后为我们执行此操作。这需要在索引指纹生成开始之前发生，以便我们稍后可以确认索引指纹生成应已覆盖 table_index_build_scan() 返回的所有元组。
		 */
		if (!fc_state->readonly)
		{
			fc_snapshot = RegisterSnapshot(GetTransactionSnapshot());

			/*
			 * GetTransactionSnapshot() 始终在 READ COMMITTED 模式下获取新的 MVCC 快照。新的快照保证包含索引所需的所有条目。
			 *
			 * 我们必须防范一个可能性，即在更高的隔离级别下返回了旧的事务快照，而该快照对目标索引的索引扫描不安全。当快照看到在索引的 indcheckxmin 边界之前的元组时，这种情况是可能的。在这里抛出错误应该是非常罕见的。似乎不值得使用辅助快照来避免这种情况。
			 */
			if (IsolationUsesXactSnapshot() && fc_rel->rd_index->indcheckxmin &&
				!TransactionIdPrecedes(HeapTupleHeaderGetXmin(fc_rel->rd_indextuple->t_data),
									   fc_snapshot->xmin))
				ereport(ERROR,
						(errcode(ERRCODE_T_R_SERIALIZATION_FAILURE),
						 errmsg("index \"%s\" cannot be verified using transaction snapshot",
								RelationGetRelationName(fc_rel))));
		}
	}

	Assert(!fc_state->rootdescend || fc_state->readonly);
	if (fc_state->rootdescend && !fc_state->heapkeyspace)
		ereport(ERROR,
				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
				 errmsg("cannot verify that tuples from index \"%s\" can each be found by an independent index search",
						RelationGetRelationName(fc_rel)),
				 errhint("Only B-Tree version 4 indexes support rootdescend verification.")));

	/* 为页面创建上下文 */
	fc_state->targetcontext = AllocSetContextCreate(CurrentMemoryContext,
												 "amcheck context",
												 ALLOCSET_DEFAULT_SIZES);
	fc_state->checkstrategy = GetAccessStrategy(BAS_BULKREAD);

	/* 从元页面获取真实的根块 */
	fc_metapage = fc_palloc_btree_page(fc_state, BTREE_METAPAGE);
	fc_metad = BTPageGetMeta(fc_metapage);

	/*
	 * 某些删除模式可能导致“瘦”B-树索引，其中快速根和真实根不同。
	 *
	 * 从真实根开始，而不是快速根，这与传统索引扫描不同。 这种方法更全面，并消除了跟随元页面中的过时快速根的风险。
	 */
	if (fc_metad->btm_fastroot != fc_metad->btm_root)
		ereport(DEBUG1,
				(errcode(ERRCODE_NO_DATA),
				 errmsg_internal("harmless fast root mismatch in index \"%s\"",
								 RelationGetRelationName(fc_rel)),
				 errdetail_internal("Fast root block %u (level %u) differs from true root block %u (level %u).",
									fc_metad->btm_fastroot, fc_metad->btm_fastlevel,
									fc_metad->btm_root, fc_metad->btm_level)));

	/*
	 * 从根开始，验证每个层级。 从左到右，从上到下。 请注意，除了元页面（元页面可以指示当索引完全为空时根为P_NONE）之外，可能没有其他页面。
	 */
	fc_previouslevel = InvalidBtreeLevel;
	fc_current.level = fc_metad->btm_level;
	fc_current.leftmost = fc_metad->btm_root;
	fc_current.istruerootlevel = true;
	while (fc_current.leftmost != P_NONE)
	{
		/*
		 * 验证这一层，如果不在叶层，则获取下一层的最左页面
		 */
		fc_current = fc_bt_check_level_from_leftmost(fc_state, fc_current);

		if (fc_current.leftmost == InvalidBlockNumber)
			ereport(ERROR,
					(errcode(ERRCODE_INDEX_CORRUPTED),
					 errmsg("index \"%s\" has no valid pages on level below %u or first level",
							RelationGetRelationName(fc_rel), fc_previouslevel)));

		fc_previouslevel = fc_current.level;
	}

	/*
	 * * 检查堆中是否包含未索引/格式不正确的元组 *
	 */
	if (fc_state->heapallindexed)
	{
		IndexInfo  *fc_indexinfo = BuildIndexInfo(fc_state->rel);
		TableScanDesc fc_scan;

		/*
		 * 为table_index_build_scan()创建我们自己的扫描，而不是让它为我们执行。 这是必要的，以便我们能够实际使用先前在!readonly情况下注册的MVCC快照。
		 *
		 * 请注意，table_index_build_scan()为我们调用heap_endscan()。
		 */
		fc_scan = table_beginscan_strat(fc_state->heaprel,	/* 关系 */
									 fc_snapshot,	/* 快照 */
									 0, /* 键的数量 */
									 NULL,	/* 扫描键 */
									 true,	/* 缓冲区访问策略OK */
									 true); /* 允许同步扫描吗？ */

		/*
		 * 扫描将在!readonly情况下表现得像CREATE INDEX CONCURRENTLY的第一次扫描。
		 *
		 * 我们不必对堆关系使用与其他ii_Concurrent调用者在!readonly情况下相同的锁强度，这没有问题。 我们没有理由关心并发VACUUM操作，因为不会有第二次堆扫描需要确保没有并发回收TID。
		 */
		fc_indexinfo->ii_Concurrent = !fc_state->readonly;

		/*
		 * 当索引是目录上的唯一索引（或被排除约束使用的索引）时，不要等待未提交元组事务提交/回滚。 否则在只读情况下可能发生这种情况。
		 */
		fc_indexinfo->ii_Unique = false;
		fc_indexinfo->ii_ExclusionOps = NULL;
		fc_indexinfo->ii_ExclusionProcs = NULL;
		fc_indexinfo->ii_ExclusionStrats = NULL;

		elog(DEBUG1, "verifying that tuples from index \"%s\" are present in \"%s\"",
			 RelationGetRelationName(fc_state->rel),
			 RelationGetRelationName(fc_state->heaprel));

		table_index_build_scan(fc_state->heaprel, fc_state->rel, fc_indexinfo, true, false,
							   fc_bt_tuple_present_callback, (void *) fc_state, fc_scan);

		ereport(DEBUG1,
				(errmsg_internal("finished verifying presence of " INT64_FORMAT " tuples from table \"%s\" with bitset %.2f%% set",
								 fc_state->heaptuplespresent, RelationGetRelationName(fc_heaprel),
								 100.0 * bloom_prop_bits_set(fc_state->filter))));

		if (fc_snapshot != SnapshotAny)
			UnregisterSnapshot(fc_snapshot);

		bloom_free(fc_state->filter);
	}

	/* 整洁： */
	MemoryContextDelete(fc_state->targetcontext);
}

/*
 * 给定某个层次的最左块，向右移动，逐个验证每个页面（对于“只读”调用者在页面之间的验证更多）。 调用者应将真实根页面作为最左页面传递，逐步向下传递这里最后一次调用返回的内容，直到达到0层（叶页面层）。
 *
 * 返回下次调用的状态（如果有的话）。 这包括应在下一个层次/调用中传递的较低一层的最左块编号，该编号在此最后一次调用时设置为P_NONE（当验证叶层时）。 层级编号遵循nbtree约定：更高层级有更高的编号，因为新的层级仅因根页面拆分而添加。 请注意，在第一次根页面拆分之前，根也是叶页面，因此始终存在0层（叶层），并且始终是最后处理的层级。
 *
 * 关于内存管理的说明：状态的每页上下文在每次调用bt_target_page_check()之间在这里重置。
 */
static BtreeLevel
fc_bt_check_level_from_leftmost(BtreeCheckState *fc_state, BtreeLevel fc_level)
{
	/* 用于建立整个层级的早期状态 */
	BTPageOpaque fc_opaque;
	MemoryContext fc_oldcontext;
	BtreeLevel	fc_nextleveldown;

	/* 用于通过右链接遍历级别的变量 */
	BlockNumber fc_leftcurrent = P_NONE;
	BlockNumber fc_current = fc_level.leftmost;

	/* 初始化返回状态 */
	fc_nextleveldown.leftmost = InvalidBlockNumber;
	fc_nextleveldown.level = InvalidBtreeLevel;
	fc_nextleveldown.istruerootlevel = false;

	/* 在本次调用期间使用页面级上下文 */
	fc_oldcontext = MemoryContextSwitchTo(fc_state->targetcontext);

	elog(DEBUG1, "verifying level %u%s", fc_level.level,
		 fc_level.istruerootlevel ?
		 " (true root level)" : fc_level.level == 0 ? " (leaf level)" : "");

	fc_state->prevrightlink = InvalidBlockNumber;
	fc_state->previncompletesplit = false;

	do
	{
		/* 不要依赖低级别的 CHECK_FOR_INTERRUPTS() 调用 */
		CHECK_FOR_INTERRUPTS();

		/* 初始化此迭代的状态 */
		fc_state->targetblock = fc_current;
		fc_state->target = fc_palloc_btree_page(fc_state, fc_state->targetblock);
		fc_state->targetlsn = PageGetLSN(fc_state->target);

		fc_opaque = BTPageGetOpaque(fc_state->target);

		if (P_IGNORE(fc_opaque))
		{
			/*
			 * 由于在只读模式下不能有并发的 VACUUM 操作，并且一旦页面被标记为完全删除，页面内部没有其他页面（兄弟和父节点）的链接，因此在只读模式下落到完全删除的页面应该是不可能的。有关更多详细信息，请参见 bt_child_check()。
			 *
			 * 此处重复 bt_child_check() 的 P_ISDELETED() 检查，以便仅通过兄弟链接可到达的页面也能被检查。
			 */
			if (fc_state->readonly && P_ISDELETED(fc_opaque))
				ereport(ERROR,
						(errcode(ERRCODE_INDEX_CORRUPTED),
						 errmsg("downlink or sibling link points to deleted block in index \"%s\"",
								RelationGetRelationName(fc_state->rel)),
						 errdetail_internal("Block=%u left block=%u left link from block=%u.",
											fc_current, fc_leftcurrent, fc_opaque->btpo_prev)));

			if (P_RIGHTMOST(fc_opaque))
				ereport(ERROR,
						(errcode(ERRCODE_INDEX_CORRUPTED),
						 errmsg("block %u fell off the end of index \"%s\"",
								fc_current, RelationGetRelationName(fc_state->rel))));
			else
				ereport(DEBUG1,
						(errcode(ERRCODE_NO_DATA),
						 errmsg_internal("block %u of index \"%s\" concurrently deleted",
										 fc_current, RelationGetRelationName(fc_state->rel))));
			goto nextpage;
		}
		else if (fc_nextleveldown.leftmost == InvalidBlockNumber)
		{
			/*
			 * 并发页面拆分可能使调用者提供的最左边块不再包含最左边页面，或者不再是真正的根，但由于重量级锁定的原因，在这种情况下检查第一个有效页面是否符合调用者的期望。
			 */
			if (fc_state->readonly)
			{
				if (!fc_bt_leftmost_ignoring_half_dead(fc_state, fc_current, fc_opaque))
					ereport(ERROR,
							(errcode(ERRCODE_INDEX_CORRUPTED),
							 errmsg("block %u is not leftmost in index \"%s\"",
									fc_current, RelationGetRelationName(fc_state->rel))));

				if (fc_level.istruerootlevel && !P_ISROOT(fc_opaque))
					ereport(ERROR,
							(errcode(ERRCODE_INDEX_CORRUPTED),
							 errmsg("block %u is not true root in index \"%s\"",
									fc_current, RelationGetRelationName(fc_state->rel))));
			}

			/*
			 * 在开始任何非平凡的级别检查之前，准备下一次 bt_check_level_from_leftmost() 调用的状态，以便为下一级（如果有的话）做好准备。
			 *
			 * 每级应该至少有一个不可忽略的页面，除非这是叶子级别，而调用者假定这是最终级别。
			 */
			if (!P_ISLEAF(fc_opaque))
			{
				IndexTuple	fc_itup;
				ItemId		fc_itemid;

				/* 内部页面 -- 向下链接在下一级获取最左边的页面 */
				fc_itemid = fc_PageGetItemIdCareful(fc_state, fc_state->targetblock,
											  fc_state->target,
											  P_FIRSTDATAKEY(fc_opaque));
				fc_itup = (IndexTuple) PageGetItem(fc_state->target, fc_itemid);
				fc_nextleveldown.leftmost = BTreeTupleGetDownLink(fc_itup);
				fc_nextleveldown.level = fc_opaque->btpo_level - 1;
			}
			else
			{
				/*
				 * 叶子页面 -- 最终级别必须由调用者处理。
				 *
				 * 注意，如果尚未发生根页面拆分，这也可能是根页面。
				 */
				fc_nextleveldown.leftmost = P_NONE;
				fc_nextleveldown.level = InvalidBtreeLevel;
			}

			/*
			 * 完成设置此调用/级别的状态。控制将永远不会在未来的循环迭代中回到此级别。
			 */
		}

		/*
		 * 兄弟链接应该相互一致。当父节点的低键向下链接的左兄弟半死时，会出现 leftcurrent == P_NONE && btpo_prev != P_NONE 的情况。（半死的页面没有来自其父节点的向下链接。）在重量级锁定下，最后的 bt_leftmost_ignoring_half_dead() 验证了此 btpo_prev。没有重量级锁定时，P_NONE 情况的验证仍未实现。
		 */
		if (fc_opaque->btpo_prev != fc_leftcurrent && fc_leftcurrent != P_NONE)
			fc_bt_recheck_sibling_links(fc_state, fc_opaque->btpo_prev, fc_leftcurrent);

		/* 检查级别 */
		if (fc_level.level != fc_opaque->btpo_level)
			ereport(ERROR,
					(errcode(ERRCODE_INDEX_CORRUPTED),
					 errmsg("leftmost down link for level points to block in index \"%s\" whose level is not one level down",
							RelationGetRelationName(fc_state->rel)),
					 errdetail_internal("Block pointed to=%u expected level=%u level in pointed to block=%u.",
										fc_current, fc_level.level, fc_opaque->btpo_level)));

		/* 验证页面的不变性 */
		fc_bt_target_page_check(fc_state);

nextpage:

		/* 尝试检测循环链接 */
		if (fc_current == fc_leftcurrent || fc_current == fc_opaque->btpo_prev)
			ereport(ERROR,
					(errcode(ERRCODE_INDEX_CORRUPTED),
					 errmsg("circular link chain found in block %u of index \"%s\"",
							fc_current, RelationGetRelationName(fc_state->rel))));

		fc_leftcurrent = fc_current;
		fc_current = fc_opaque->btpo_next;

		if (fc_state->lowkey)
		{
			Assert(fc_state->readonly);
			pfree(fc_state->lowkey);
			fc_state->lowkey = NULL;
		}

		/*
		 * 将当前目标的高键复制为右兄弟的低键。
		 * 在上层上下文中分配内存，这样在重置目标上下文后会被清除。
		 *
		 * 我们只在检查子高键的边缘情况下需要低键。只有当子级的未完成拆分
		 * 落在目标级别页面的边界上时，我们才使用高键。有关详细信息，请参见
		 * bt_child_highkey_check()。因此，通常我们不会对低键进行操作，
		 * 但为了高键验证的一般情况，始终提供低键会更简单。
		 *
		 * 在并发拆分的情况下管理低键的正确性尚未调查。谢天谢地，
		 * 我们只需要低键进行只读验证，而并发拆分不会发生。
		 */
		if (fc_state->readonly && !P_RIGHTMOST(fc_opaque))
		{
			IndexTuple	fc_itup;
			ItemId		fc_itemid;

			fc_itemid = fc_PageGetItemIdCareful(fc_state, fc_state->targetblock,
										  fc_state->target, P_HIKEY);
			fc_itup = (IndexTuple) PageGetItem(fc_state->target, fc_itemid);

			fc_state->lowkey = MemoryContextAlloc(fc_oldcontext, IndexTupleSize(fc_itup));
			memcpy(fc_state->lowkey, fc_itup, IndexTupleSize(fc_itup));
		}

		/* 释放页面和此迭代关联的内存 */
		MemoryContextReset(fc_state->targetcontext);
	}
	while (fc_current != P_NONE);

	if (fc_state->lowkey)
	{
		Assert(fc_state->readonly);
		pfree(fc_state->lowkey);
		fc_state->lowkey = NULL;
	}

	/* 不要更改调用者的上下文 */
	MemoryContextSwitchTo(fc_oldcontext);

	return fc_nextleveldown;
}

/*
 * 像 P_LEFTMOST(start_opaque)，但接受任意长度的半死、兄弟链接的页面链到左边。
 * 如果在 state->readonly 下出现半死页面，数据库在删除的第一阶段和第二阶段
 * WAL 记录之间退出了恢复。
 */
static bool fc_bt_leftmost_ignoring_half_dead(BtreeCheckState *fc_state,
							   BlockNumber fc_start,
							   BTPageOpaque fc_start_opaque)
{
	BlockNumber fc_reached = fc_start_opaque->btpo_prev,
				fc_reached_from = fc_start;
	bool		fc_all_half_dead = true;

	/*
	 * 为了处理 !readonly 情况，我们需要接受 BTP_DELETED 页面，并可能观察
	 * nbtree/README "页面删除和向后扫描"。
	 */
	Assert(fc_state->readonly);

	while (fc_reached != P_NONE && fc_all_half_dead)
	{
		Page		fc_page = fc_palloc_btree_page(fc_state, fc_reached);
		BTPageOpaque fc_reached_opaque = BTPageGetOpaque(fc_page);

		CHECK_FOR_INTERRUPTS();

		/*
		 * 尝试检测 btpo_prev 循环链接。 _bt_unlink_halfdead_page()
		 * 写入侧链接将继续指向兄弟页面。
		 * 检查 btpo_next 是否具有该属性。
		 */
		fc_all_half_dead = P_ISHALFDEAD(fc_reached_opaque) &&
			fc_reached != fc_start &&
			fc_reached != fc_reached_from &&
			fc_reached_opaque->btpo_next == fc_reached_from;
		if (fc_all_half_dead)
		{
			XLogRecPtr	fc_pagelsn = PageGetLSN(fc_page);

			/* pagelsn 应指向一个 XLOG_BTREE_MARK_PAGE_HALFDEAD */
			ereport(DEBUG1,
					(errcode(ERRCODE_NO_DATA),
					 errmsg_internal("harmless interrupted page deletion detected in index \"%s\"",
									 RelationGetRelationName(fc_state->rel)),
					 errdetail_internal("Block=%u right block=%u page lsn=%X/%X.",
										fc_reached, fc_reached_from,
										LSN_FORMAT_ARGS(fc_pagelsn))));

			fc_reached_from = fc_reached;
			fc_reached = fc_reached_opaque->btpo_prev;
		}

		pfree(fc_page);
	}

	return fc_all_half_dead;
}

/*
 * 当目标页面的左链接未指向上一个目标页面时，引发错误，这里称为leftcurrent。 
 * 为到达当前目标页面，左当前页面的右链接被跟随，我们期望 
 * leftcurrent和当前目标页面之间存在一致性。确保在!readonly情况下， 
 * 这个条件已经被违反，因为并发页面分裂是我们需要处理的问题。
 *
 * 涉及页面之间的不一致性，如它们不一致地被认为是兄弟页面， 
 * 已知是部分写入/丢失更新相关损坏的一个特别好的指标。 
 * bt_right_page_check_scankey检查也提供了一种检测不一致性的方法 
 * ，但它只能检测有乱序键空间的兄弟页面， 
 * 这不能捕捉到我们期望在这里捕捉的许多问题。
 *
 * 这种不一致性的经典示例是我们只能通过此检查捕捉到的 
 * （在!readonly模式下）涉及三个兄弟页面的情况， 
 * 这些页面在过去的某个时候受到错误页面分裂的影响。 
 * 分裂的影响在原始页面及其新的右兄弟页面上反映出来， 
 * 原始右兄弟页面缺乏任何伴随的变化。 
 * 原始右兄弟页面的左链接未能指向新的右兄弟页面 
 * （它的左链接仍指向原始页面）， 
 * 即使页面分裂的第一阶段应该作为一个原子操作工作。 
 * 这种微妙的不一致在实践中可能只会破坏向后扫描。
 *
 * 请注意，这是amcheck将“耦合”缓冲区锁的位置 
 * （仅针对!readonly调用者）。 
 * 一般来说，我们希望在!readonly模式下避免更彻底的跨页面检查， 
 * 但在这里似乎值得这种复杂性。 
 * 此外，在这里执行锁耦合的性能开销在实践中是微不足道的。 
 * 控制仅在存在非损坏索引时到达此处，当 
 * 调用者在与左当前页面的交替中跨越到目标页面时有一个并发页面分裂。
 */
static void fc_bt_recheck_sibling_links(BtreeCheckState *fc_state,
						 BlockNumber fc_btpo_prev_from_target,
						 BlockNumber fc_leftcurrent)
{
	/* 将metapage传递给BTPageGetOpaque()将产生无关的发现 */
	Assert(fc_leftcurrent != P_NONE);

	if (!fc_state->readonly)
	{
		Buffer		fc_lbuf;
		Buffer		fc_newtargetbuf;
		Page		fc_page;
		BTPageOpaque fc_opaque;
		BlockNumber fc_newtargetblock;

		/* 按照通常的顺序耦合锁：从左到右 */
		fc_lbuf = ReadBufferExtended(fc_state->rel, MAIN_FORKNUM, fc_leftcurrent,
								  RBM_NORMAL, fc_state->checkstrategy);
		LockBuffer(fc_lbuf, BT_READ);
		_bt_checkpage(fc_state->rel, fc_lbuf);
		fc_page = BufferGetPage(fc_lbuf);
		fc_opaque = BTPageGetOpaque(fc_page);
		if (P_ISDELETED(fc_opaque))
		{
			/*
			 * 无法推理关于并发删除页面的情况 -- 页面右侧的左链接
			 * 预计指向位于左侧的其他页面（不是leftcurrent页面）。
			 *
			 * 注意，我们故意不放弃半死的页面。
			 */
			UnlockReleaseBuffer(fc_lbuf);
			return;
		}

		fc_newtargetblock = fc_opaque->btpo_next;
		/* 避免自死锁，当newtargetblock == leftcurrent时 */
		if (fc_newtargetblock != fc_leftcurrent)
		{
			fc_newtargetbuf = ReadBufferExtended(fc_state->rel, MAIN_FORKNUM,
											  fc_newtargetblock, RBM_NORMAL,
											  fc_state->checkstrategy);
			LockBuffer(fc_newtargetbuf, BT_READ);
			_bt_checkpage(fc_state->rel, fc_newtargetbuf);
			fc_page = BufferGetPage(fc_newtargetbuf);
			fc_opaque = BTPageGetOpaque(fc_page);
			/* btpo_prev_from_target可能已经改变；进行更新 */
			fc_btpo_prev_from_target = fc_opaque->btpo_prev;
		}
		else
		{
			/*
			 * leftcurrent的右兄弟指向leftcurrent区块。
			 * 索引已损坏。处理此问题的最简单方法是 
			 * 假装我们实际上是从一个在其btpo_prev中具有无效 
			 * 块号的不同页面读取的。
			 */
			fc_newtargetbuf = InvalidBuffer;
			fc_btpo_prev_from_target = InvalidBlockNumber;
		}

		/*
		 * 在此不需要检查 P_ISDELETED，因为只要我们持有 lbuf 上的锁，
		 * 新目标块就不能被标记为已删除
		 */
		if (BufferIsValid(fc_newtargetbuf))
			UnlockReleaseBuffer(fc_newtargetbuf);
		UnlockReleaseBuffer(fc_lbuf);

		if (fc_btpo_prev_from_target == fc_leftcurrent)
		{
			/* 报告左兄弟中的分裂，而不是目标（或新目标） */
			ereport(DEBUG1,
					(errcode(ERRCODE_INTERNAL_ERROR),
					 errmsg_internal("harmless concurrent page split detected in index \"%s\"",
									 RelationGetRelationName(fc_state->rel)),
					 errdetail_internal("Block=%u new right sibling=%u original right sibling=%u.",
										fc_leftcurrent, fc_newtargetblock,
										fc_state->targetblock)));
			return;
		}

		/*
		 * 索引损坏。 确保我们报告正确的目标页。
		 *
		 * 在存在并发页分裂以及索引损坏的情况下（至少理论上），
		 * 这可能已经改变。 请注意，btpo_prev_from_target 已经在上面更新。
		 */
		fc_state->targetblock = fc_newtargetblock;
	}

	ereport(ERROR,
			(errcode(ERRCODE_INDEX_CORRUPTED),
			 errmsg("left link/right link pair in index \"%s\" not in agreement",
					RelationGetRelationName(fc_state->rel)),
			 errdetail_internal("Block=%u left block=%u left link from block=%u.",
								fc_state->targetblock, fc_leftcurrent,
								fc_btpo_prev_from_target)));
}

/*
 * 函数对目标页或与目标页相关的页执行以下检查：
 *
 * - 每个“真实”数据项都小于或等于高键，这是页上项的上限。
 *   当页是内部页时，数据项应严格小于高键。
 *
 * - 在页内，每个数据项严格小于其右侧紧邻的项（如果有的话），
 *   （即，项在页内有序，以便索引扫描执行的二进制搜索是
 *   合理的）。
 *
 * - 存储在页上的最后一个数据项严格小于右侧页上第一个数据项
 *   （当这样的第一个项可用时）。
 *
 * - 对元组本身结构的各种检查。例如，检查
 *   非枢轴元组没有截断的属性。
 *
 * 此外，当传递的状态显示持有 ShareLock 时，函数还检查：
 *
 * - 所有子页都遵守来自父级的枢轴元组的严格下限。
 *
 * - 在期望的父级中遇到指向块的下行链接。
 *
 * - 子页的高键与父页中的对应枢轴键匹配。
 *
 * 这也是 heapallindexed 调用者使用其布隆过滤器
 * 对 IndexTuples 进行指纹识别以便于后续的 table_index_build_scan() 验证的地方。
 *
 * 注意：此例程中分配的内存预期由调用者通过重置 state->targetcontext 来释放。
 */
static void fc_bt_target_page_check(BtreeCheckState *fc_state)
{
	OffsetNumber fc_offset;
	OffsetNumber fc_max;
	BTPageOpaque fc_topaque;

	fc_topaque = BTPageGetOpaque(fc_state->target);
	fc_max = PageGetMaxOffsetNumber(fc_state->target);

	elog(DEBUG2, "verifying %u items on %s block %u", fc_max,
		 P_ISLEAF(fc_topaque) ? "leaf" : "internal", fc_state->targetblock);

	/*
	 * 检查高键中的属性数量。 注意，最右侧的页面
	 * 不包含高键，因此无需检查
	 */
	if (!P_RIGHTMOST(fc_topaque))
	{
		ItemId		fc_itemid;
		IndexTuple	fc_itup;

		/* 在检查元组之前验证行指针 */
		fc_itemid = fc_PageGetItemIdCareful(fc_state, fc_state->targetblock,
									  fc_state->target, P_HIKEY);
		if (!_bt_check_natts(fc_state->rel, fc_state->heapkeyspace, fc_state->target,
							 P_HIKEY))
		{
			fc_itup = (IndexTuple) PageGetItem(fc_state->target, fc_itemid);
			ereport(ERROR,
					(errcode(ERRCODE_INDEX_CORRUPTED),
					 errmsg("wrong number of high key index tuple attributes in index \"%s\"",
							RelationGetRelationName(fc_state->rel)),
					 errdetail_internal("Index block=%u natts=%u block type=%s page lsn=%X/%X.",
										fc_state->targetblock,
										BTreeTupleGetNAtts(fc_itup, fc_state->rel),
										P_ISLEAF(fc_topaque) ? "heap" : "index",
										LSN_FORMAT_ARGS(fc_state->targetlsn))));
		}
	}

	/*
	 * 遍历页面项，从第一个非高键项开始，而不是高键
	 * （如果有的话）。 对于“负无穷大”
	 * 真实项（如果有的话），大多数测试不会执行。
	 */
	for (fc_offset = P_FIRSTDATAKEY(fc_topaque);
		 fc_offset <= fc_max;
		 fc_offset = OffsetNumberNext(fc_offset))
	{
		ItemId		fc_itemid;
		IndexTuple	fc_itup;
		size_t		fc_tupsize;
		BTScanInsert fc_skey;
		bool		fc_lowersizelimit;
		ItemPointer fc_scantid;

		CHECK_FOR_INTERRUPTS();

		fc_itemid = fc_PageGetItemIdCareful(fc_state, fc_state->targetblock,
									  fc_state->target, fc_offset);
		fc_itup = (IndexTuple) PageGetItem(fc_state->target, fc_itemid);
		fc_tupsize = IndexTupleSize(fc_itup);

		/*
		 * lp_len 应与报告的 IndexTuple 长度完全匹配，
		 * 因为 lp_len 在索引中是完全冗余的，并且两种
		 * 元组长度来源都是 MAXALIGN() 的。 nbtree 并不
		 * 经常使用 lp_len，并且对此类损坏的 lp_len 字段
		 * 出乎意料地宽容。
		 */
		if (fc_tupsize != ItemIdGetLength(fc_itemid))
			ereport(ERROR,
					(errcode(ERRCODE_INDEX_CORRUPTED),
					 errmsg("index tuple size does not equal lp_len in index \"%s\"",
							RelationGetRelationName(fc_state->rel)),
					 errdetail_internal("Index tid=(%u,%u) tuple size=%zu lp_len=%u page lsn=%X/%X.",
										fc_state->targetblock, fc_offset,
										fc_tupsize, ItemIdGetLength(fc_itemid),
										LSN_FORMAT_ARGS(fc_state->targetlsn)),
					 errhint("This could be a torn page problem.")));

		/* 检查索引元组属性的数量 */
		if (!_bt_check_natts(fc_state->rel, fc_state->heapkeyspace, fc_state->target,
							 fc_offset))
		{
			ItemPointer fc_tid;
			char	   *fc_itid,
					   *fc_htid;

			fc_itid = psprintf("(%u,%u)", fc_state->targetblock, fc_offset);
			fc_tid = fc_BTreeTupleGetPointsToTID(fc_itup);
			fc_htid = psprintf("(%u,%u)",
							ItemPointerGetBlockNumberNoCheck(fc_tid),
							ItemPointerGetOffsetNumberNoCheck(fc_tid));

			ereport(ERROR,
					(errcode(ERRCODE_INDEX_CORRUPTED),
					 errmsg("wrong number of index tuple attributes in index \"%s\"",
							RelationGetRelationName(fc_state->rel)),
					 errdetail_internal("Index tid=%s natts=%u points to %s tid=%s page lsn=%X/%X.",
										fc_itid,
										BTreeTupleGetNAtts(fc_itup, fc_state->rel),
										P_ISLEAF(fc_topaque) ? "heap" : "index",
										fc_htid,
										LSN_FORMAT_ARGS(fc_state->targetlsn))));
		}

		/*
		 * 不要尝试使用“负无穷大”项在内部页上生成扫描键。
		 * 它们总是被截断为零属性。
		 */
		if (fc_offset_is_negative_infinity(fc_topaque, fc_offset))
		{
			/*
			 * 我们不对“负无穷大”项调用 bt_child_check()。
			 * 但是如果我们正在进行下行连接性检查，我们会对
			 * 每个项（包括“负无穷大”项）执行此检查。
			 */
			if (!P_ISLEAF(fc_topaque) && fc_state->readonly)
			{
				fc_bt_child_highkey_check(fc_state,
									   fc_offset,
									   NULL,
									   fc_topaque->btpo_level);
			}
			continue;
		}

		/*
		 * 只读调用者可以选择验证非支点元组是否
		 * 可以通过从根开始的独立搜索找到。
		 * 请注意，我们故意不对每个
		 * TID 进行单独搜索，因为发布列表本身已通过其他检查验证。
		 */
		if (fc_state->rootdescend && P_ISLEAF(fc_topaque) &&
			!fc_bt_rootdescend(fc_state, fc_itup))
		{
			ItemPointer fc_tid = fc_BTreeTupleGetPointsToTID(fc_itup);
			char	   *fc_itid,
					   *fc_htid;

			fc_itid = psprintf("(%u,%u)", fc_state->targetblock, fc_offset);
			fc_htid = psprintf("(%u,%u)", ItemPointerGetBlockNumber(fc_tid),
							ItemPointerGetOffsetNumber(fc_tid));

			ereport(ERROR,
					(errcode(ERRCODE_INDEX_CORRUPTED),
					 errmsg("could not find tuple using search from root page in index \"%s\"",
							RelationGetRelationName(fc_state->rel)),
					 errdetail_internal("Index tid=%s points to heap tid=%s page lsn=%X/%X.",
										fc_itid, fc_htid,
										LSN_FORMAT_ARGS(fc_state->targetlsn))));
		}

		/*
		 * 如果元组是发布列表元组，请确保发布列表 TIDs
		 * 是有序的
		 */
		if (BTreeTupleIsPosting(fc_itup))
		{
			ItemPointerData fc_last;
			ItemPointer fc_current;

			ItemPointerCopy(BTreeTupleGetHeapTID(fc_itup), &fc_last);

			for (int fc_i = 1; fc_i < BTreeTupleGetNPosting(fc_itup); fc_i++)
			{

				fc_current = BTreeTupleGetPostingN(fc_itup, fc_i);

				if (ItemPointerCompare(fc_current, &fc_last) <= 0)
				{
					char	   *fc_itid = psprintf("(%u,%u)", fc_state->targetblock, fc_offset);

					ereport(ERROR,
							(errcode(ERRCODE_INDEX_CORRUPTED),
							 errmsg_internal("posting list contains misplaced TID in index \"%s\"",
											 RelationGetRelationName(fc_state->rel)),
							 errdetail_internal("Index tid=%s posting list offset=%d page lsn=%X/%X.",
												fc_itid, fc_i,
												LSN_FORMAT_ARGS(fc_state->targetlsn))));
				}

				ItemPointerCopy(fc_current, &fc_last);
			}
		}

		/* 为当前页面偏移量构建插入扫描键 */
		fc_skey = fc_bt_mkscankey_pivotsearch(fc_state->rel, fc_itup);

		/*
		 * 确保元组大小不超过相关的 BTREE_VERSION
		 * 特定限制。
		 *
		 * BTREE_VERSION 4（引入堆键空间规则）从 BTMaxItemSize()
		 * 中申请了一小部分空间，以确保后缀截断始终有足够的空间
		 * 将显式的堆 TID 添加回元组——我们悲观地假设每个
		 * 新插入的元组最终都需要在未来的叶页面拆分期间
		 * 附加一个堆 TID，当该元组成为新的高键（支点元组）
		 * 的基础时。
		 *
		 * 由于回收的空间是为此目的保留的，
		 * 当额外空间按预期被使用时，我们不能实施稍低的限制。
		 * 换句话说，叶页面中元组大小的限制仅在版本间
		 * 存在差异。
		 *
		 * 不过，我们对 BTREE_VERSION 4
		 * 内部页面的细节仍然很谨慎。支点元组只能将额外空间用于其
		 * 指定目的。当显式的堆 TID 实际上不存在时，支点元组应该
		 * 执行较低限制。（在所有其他情况下
		 * 后缀截断保证生成的支点元组不
		 * 大于调用者提供的 firstright 元组。）
		 */
		fc_lowersizelimit = fc_skey->heapkeyspace &&
			(P_ISLEAF(fc_topaque) || BTreeTupleGetHeapTID(fc_itup) == NULL);
		if (fc_tupsize > (fc_lowersizelimit ? BTMaxItemSize(fc_state->target) :
					   BTMaxItemSizeNoHeapTid(fc_state->target)))
		{
			ItemPointer fc_tid = fc_BTreeTupleGetPointsToTID(fc_itup);
			char	   *fc_itid,
					   *fc_htid;

			fc_itid = psprintf("(%u,%u)", fc_state->targetblock, fc_offset);
			fc_htid = psprintf("(%u,%u)",
							ItemPointerGetBlockNumberNoCheck(fc_tid),
							ItemPointerGetOffsetNumberNoCheck(fc_tid));

			ereport(ERROR,
					(errcode(ERRCODE_INDEX_CORRUPTED),
					 errmsg("index row size %zu exceeds maximum for index \"%s\"",
							fc_tupsize, RelationGetRelationName(fc_state->rel)),
					 errdetail_internal("Index tid=%s points to %s tid=%s page lsn=%X/%X.",
										fc_itid,
										P_ISLEAF(fc_topaque) ? "heap" : "index",
										fc_htid,
										LSN_FORMAT_ARGS(fc_state->targetlsn))));
		}

		/* 对指向堆的叶页面元组进行指纹识别 */
		if (fc_state->heapallindexed && P_ISLEAF(fc_topaque) && !ItemIdIsDead(fc_itemid))
		{
			IndexTuple	fc_norm;

			if (BTreeTupleIsPosting(fc_itup))
			{
				/* 将所有元素标识为不同的“普通”元组 */
				for (int fc_i = 0; fc_i < BTreeTupleGetNPosting(fc_itup); fc_i++)
				{
					IndexTuple	fc_logtuple;

					fc_logtuple = fc_bt_posting_plain_tuple(fc_itup, fc_i);
					fc_norm = fc_bt_normalize_tuple(fc_state, fc_logtuple);
					bloom_add_element(fc_state->filter, (unsigned char *) fc_norm,
									  IndexTupleSize(fc_norm));
					/* 保持整洁 */
					if (fc_norm != fc_logtuple)
						pfree(fc_norm);
					pfree(fc_logtuple);
				}
			}
			else
			{
				fc_norm = fc_bt_normalize_tuple(fc_state, fc_itup);
				bloom_add_element(fc_state->filter, (unsigned char *) fc_norm,
								  IndexTupleSize(fc_norm));
				/* 保持整洁 */
				if (fc_norm != fc_itup)
					pfree(fc_norm);
			}
		}

		/*
		 * * 高键检查 *
		 *
		 * 如果存在一个高键（如果这不是其整个层级上的最右页面），检查高键是否确实是所有页面项目的上限。如果这是一个发布列表元组，我们需要将scantid设置为发布列表中的最高TID。
		 *
		 * 我们更倾向于将所有项目与高键进行检查，而不是仅检查最后一个并相信操作符类遵守传递法则（这意味着如果它们通过了项目顺序检查，则所有先前项目也遵守高键不变性）。
		 *
		 * 理想情况下，我们会将索引中的每个项目与索引中的每个其他项目进行比较，而不是依赖操作类对传递法则的遵守来填补子项与其祖父项（以及曾祖父项等）之间的空白。我们不这样做，因为这样成本过高，而且在实践中可能效果也不会显著提升。
		 *
		 * 在叶子层级上，我们检查键是否 <= 高键。然而，在非叶子层级上，我们检查键是否 < 高键，因为高键是“只是另一个分隔符”，而不是某些现有键项目的副本；我们期望它在同一层级的所有键中是唯一的。（后缀截断有时会产生一个未截断的叶子高键，该高键是最后左侧项目的副本，但绝不会是其他任何项目，这使得我们必须将叶子层级检查减弱为 <=。）
		 *
		 * 促使高键从未真正成为来自同层级其他项目的副本的完整解释：
		 *
		 * 虽然新左页面的高键在内部页面拆分期间是从右页面的第一个偏移量复制的，但这并不是全部故事。实际上，内部页面是在第一个右侧元组的中间拆分的，而不是在原本会是最后左侧和第一个右侧元组之间：第一个右侧键最终位于左侧，作为左侧的新高键，而第一个右侧下行链接则位于右侧，作为右侧的新“负无穷”项目。负无穷元组被截断为零属性，因此我们只剩下下行链接。换句话说，复制只是（支点）元组中间拆分的一个实现细节。（另见：nbtree README中的“关于数据表示的说明”。）
		 */
		fc_scantid = fc_skey->scantid;
		if (fc_state->heapkeyspace && BTreeTupleIsPosting(fc_itup))
			fc_skey->scantid = BTreeTupleGetMaxHeapTID(fc_itup);

		if (!P_RIGHTMOST(fc_topaque) &&
			!(P_ISLEAF(fc_topaque) ? fc_invariant_leq_offset(fc_state, fc_skey, P_HIKEY) :
			  fc_invariant_l_offset(fc_state, fc_skey, P_HIKEY)))
		{
			ItemPointer fc_tid = fc_BTreeTupleGetPointsToTID(fc_itup);
			char	   *fc_itid,
					   *fc_htid;

			fc_itid = psprintf("(%u,%u)", fc_state->targetblock, fc_offset);
			fc_htid = psprintf("(%u,%u)",
							ItemPointerGetBlockNumberNoCheck(fc_tid),
							ItemPointerGetOffsetNumberNoCheck(fc_tid));

			ereport(ERROR,
					(errcode(ERRCODE_INDEX_CORRUPTED),
					 errmsg("high key invariant violated for index \"%s\"",
							RelationGetRelationName(fc_state->rel)),
					 errdetail_internal("Index tid=%s points to %s tid=%s page lsn=%X/%X.",
										fc_itid,
										P_ISLEAF(fc_topaque) ? "heap" : "index",
										fc_htid,
										LSN_FORMAT_ARGS(fc_state->targetlsn))));
		}
		/* 重置，以防scantid被设置为(itup)发布元组的最大TID */
		fc_skey->scantid = fc_scantid;

		/*
		 * * 项目顺序检查 *
		 *
		 * 通过检查当前项目是否严格小于下一个项目（如果有），检查项目是否以逻辑顺序存储在页面上。
		 */
		if (OffsetNumberNext(fc_offset) <= fc_max &&
			!fc_invariant_l_offset(fc_state, fc_skey, OffsetNumberNext(fc_offset)))
		{
			ItemPointer fc_tid;
			char	   *fc_itid,
					   *fc_htid,
					   *fc_nitid,
					   *fc_nhtid;

			fc_itid = psprintf("(%u,%u)", fc_state->targetblock, fc_offset);
			fc_tid = fc_BTreeTupleGetPointsToTID(fc_itup);
			fc_htid = psprintf("(%u,%u)",
							ItemPointerGetBlockNumberNoCheck(fc_tid),
							ItemPointerGetOffsetNumberNoCheck(fc_tid));
			fc_nitid = psprintf("(%u,%u)", fc_state->targetblock,
							 OffsetNumberNext(fc_offset));

			/* 重用itup以获取第二个项目指向的堆位置 */
			fc_itemid = fc_PageGetItemIdCareful(fc_state, fc_state->targetblock,
										  fc_state->target,
										  OffsetNumberNext(fc_offset));
			fc_itup = (IndexTuple) PageGetItem(fc_state->target, fc_itemid);
			fc_tid = fc_BTreeTupleGetPointsToTID(fc_itup);
			fc_nhtid = psprintf("(%u,%u)",
							 ItemPointerGetBlockNumberNoCheck(fc_tid),
							 ItemPointerGetOffsetNumberNoCheck(fc_tid));

			ereport(ERROR,
					(errcode(ERRCODE_INDEX_CORRUPTED),
					 errmsg("item order invariant violated for index \"%s\"",
							RelationGetRelationName(fc_state->rel)),
					 errdetail_internal("Lower index tid=%s (points to %s tid=%s) "
										"higher index tid=%s (points to %s tid=%s) "
										"page lsn=%X/%X.",
										fc_itid,
										P_ISLEAF(fc_topaque) ? "heap" : "index",
										fc_htid,
										fc_nitid,
										P_ISLEAF(fc_topaque) ? "heap" : "index",
										fc_nhtid,
										LSN_FORMAT_ARGS(fc_state->targetlsn))));
		}

		/*
		 * * 最后一个项目检查 *
		 *
		 * 在达到页面的最后一项时，检查最后一个项目与下一个/右页面的第一个数据项。这个附加检查将检测跨页，如果假定的右兄弟页面恰好在键空间中位于目标之前。（否则，后续的堆验证可能会检测到问题。）
		 *
		 * 这个检查类似于在检查最后一项时，对目标页面上每个其他“真实”项已经执行的项顺序检查。不同之处在于，下一个项（与目标的最后一项进行比较的项）需要来自下一个/兄弟页面。然而，由于各种原因，可能没有来自兄弟的这样的项（例如，目标是该层的最右页面）。
		 */
		else if (fc_offset == fc_max)
		{
			BTScanInsert fc_rightkey;

			/* 获取下一个/右页面中的项目 */
			fc_rightkey = fc_bt_right_page_check_scankey(fc_state);

			if (fc_rightkey &&
				!fc_invariant_g_offset(fc_state, fc_rightkey, fc_max))
			{
				/*
				 * 如在 bt_right_page_check_scankey() 中详细解释，有一个已知的非只读竞争条件可能导致不变量的明显违反，必须在实际提出错误之前检查。我们的防护条件是目标页面已被删除。
				 */
				if (!fc_state->readonly)
				{
					/* 获取目标页面的新副本 */
					fc_state->target = fc_palloc_btree_page(fc_state, fc_state->targetblock);
					/* 请注意，我们故意不更新目标的 LSN */
					fc_topaque = BTPageGetOpaque(fc_state->target);

					/*
					 * 所有非只读检查现在都已执行；只需返回
					 */
					if (P_IGNORE(fc_topaque))
						return;
				}

				ereport(ERROR,
						(errcode(ERRCODE_INDEX_CORRUPTED),
						 errmsg("cross page item order invariant violated for index \"%s\"",
								RelationGetRelationName(fc_state->rel)),
						 errdetail_internal("Last item on page tid=(%u,%u) page lsn=%X/%X.",
											fc_state->targetblock, fc_offset,
											LSN_FORMAT_ARGS(fc_state->targetlsn))));
			}
		}

		/*
		 * * 下行链接检查 *
		 *
		 * 额外检查子项，前提是这是一个内部页面，并且调用者持有共享锁。这发生在目标中的每个下行链接（项）上，除了负无穷下行链接（同样，这是因为它没有有用的值进行比较）。
		 */
		if (!P_ISLEAF(fc_topaque) && fc_state->readonly)
			fc_bt_child_check(fc_state, fc_skey, fc_offset);
	}

	/*
	 * 特殊情况 bt_child_highkey_check() 调用
	 *
	 * 我们不传递真实的下行链接，但我们需要完成级别处理。如果条件满足，我们已经处理了目标级别中的所有下行链接。但在我们最右下行链接所指向的子页面的右侧仍可能有页面。它们可能会缺少下行链接。这个最后的调用检查它们。
	 */
	if (!P_ISLEAF(fc_topaque) && P_RIGHTMOST(fc_topaque) && fc_state->readonly)
	{
		fc_bt_child_highkey_check(fc_state, InvalidOffsetNumber,
							   NULL, fc_topaque->btpo_level);
	}
}

/*
 * 返回一个用于当前目标右侧页面（或第一个不可忽略页面）上项目的扫描键，足以检查当前目标页面中最后一项的顺序不变量。返回的扫描键依赖于为子页面分配的本地内存，调用者无法进行释放。调用者的内存上下文应在此处的调用之间重置。
 *
 * 这是第一个数据项，因此所有相邻项都与其直接兄弟项进行检查（可能在兄弟页面上，或甚至在目标的右链接指向不同父页面的页面的父边界上的“表亲”页面上）。如果没有此类有效项，返回 NULL。
 *
 * 请注意，非只读调用者必须重新确认目标页面没有被并发删除。
 */
static BTScanInsert
fc_bt_right_page_check_scankey(BtreeCheckState *fc_state)
{
	BTPageOpaque fc_opaque;
	ItemId		fc_rightitem;
	IndexTuple	fc_firstitup;
	BlockNumber fc_targetnext;
	Page		fc_rightpage;
	OffsetNumber fc_nline;

	/* 确定目标的下一个块编号 */
	fc_opaque = BTPageGetOpaque(fc_state->target);

	/* 如果目标已经是最右侧，则没有右兄弟；这里无需做任何事情 */
	if (P_RIGHTMOST(fc_opaque))
		return NULL;

	/*
	 * 关于并发页面拆分和页面删除的一般说明：
	 *
	 * 像 _bt_search() 这样的例程在遍历树时不需要任何页面拆分互锁，
	 * 包括像缓冲区固定这样的非常轻的操作。这就是为什么我们也不需要
	 * 这样做的原因。实际上，这种避免“耦合”缓冲锁的需求是 Lehman & Yao
	 * 算法的存在理由。
	 *
	 * 这样就留下了删除。一页被删除后，VACUUM 不会在足够早的时间
	 * 回收它，以至于我们至少不能跟随其右链接（或左链接，或下链接）
	 * 找到它的兄弟，因为回收直到没有可能的索引扫描能够落在该页面
	 * 上时才会发生。索引扫描可以跟随链接，只需其快照作为互锁，
	 * 并确保至少如此。（见 nbtree README 中的页面回收/“对所有人可见”说明。）
	 *
	 * 此外，如果我们跟随一个右链接并找到一个半死或死亡（可忽略）的
	 * 页面一次或多次也是可以的。我们要么会有一个进一步的右链接
	 * 跟随，它会在不久之后通向一个活页（在经过父项的最右子项之前），
	 * 或者我们将找到整个层级的结束（当父页面本身就是其层级的最右
	 * 时就有可能发生）。
	 */
	fc_targetnext = fc_opaque->btpo_next;
	for (;;)
	{
		CHECK_FOR_INTERRUPTS();

		fc_rightpage = fc_palloc_btree_page(fc_state, fc_targetnext);
		fc_opaque = BTPageGetOpaque(fc_rightpage);

		if (!P_IGNORE(fc_opaque) || P_RIGHTMOST(fc_opaque))
			break;

		/*
		 * 我们落在一个已删除或半死的兄弟页面上。向右移动，直到
		 * 我们找到一个活的兄弟页面。
		 */
		ereport(DEBUG2,
				(errcode(ERRCODE_NO_DATA),
				 errmsg_internal("level %u sibling page in block %u of index \"%s\" was found deleted or half dead",
								 fc_opaque->btpo_level, fc_targetnext, RelationGetRelationName(fc_state->rel)),
				 errdetail_internal("Deleted page found when building scankey from right sibling.")));

		fc_targetnext = fc_opaque->btpo_next;

		/* 稍微积极一些以释放这块内存，以防万一 */
		pfree(fc_rightpage);
	}

	/*
	 * 没有持有 ShareLock 的情况 -- 为什么可以安全地继续。
	 *
	 * 问题：
	 *
	 * 我们必须避免当调用者将这里返回的项目视为目标最后项目的上界时产生误报的损坏。在一般情况下，不允许出现误报。在调用者为非只读时避免它们是微妙的。
	 *
	 * 对目标页面的并发页面删除可能导致在此右兄弟页面上插入本应在目标页面上插入的项目。可能在目标的下链指向右兄弟而不是目标后，存在在目标下链之后的插入。插入者插入的项目本应属于目标页面。这种竞争非常紧密，但这是可能的。这是我们的唯一问题。
	 *
	 * 非问题：
	 *
	 * 我们不会受到目标的并发页面拆分的阻碍；无论如何我们永远不会落在页面的后半部分。右侧页面的并发拆分也无关紧要，因为我们可靠落在的左半部分中的第一个数据项目保持不变。如果我们必须跳过可忽略/已删除的页面，那也无关紧要，因为它们的键空间已经与我们最终找到的第一个非可忽略页面原子地合并（我们最终找到的页面是目标的真正兄弟还是表亲无关紧要，我们将在下面解释）。
	 *
	 * 解决方案：
	 *
	 * 调用者知道在跨页兄弟项目比较显示损坏（不变量失败）时，应重新验证目标是否可忽略（半死或已删除）。这检测了对于调用者存在的单一竞争条件。这是正确的，因为目标块作为非可忽略（并非半死或已删除）继续存在意味着目标页面没有被右侧的删除合并；目标的关键空间在目标之后从未向左移动。目标的父页面要么具有与之前相同的下链指向目标，要么由于目标左侧的删除而具有 < 下链。目标要么具有与之前相同的高键，要么在页面拆分时高键 < 之前。（与目标页面并发拆分的最右侧页面仍将具有与目标最初找到的相同的高键，对于我们的目的等同于目标的高键本身从未改变，因为我们可靠地跳过从目标页面并发拆分的页面。）
	 *
	 * 用更简单的话说，我们允许目标的键空间向左扩展（键空间只能在目标的左侧向左移动），但目标键空间不能向右扩展并在我们检测之前超前。目标的键空间不能缩小，除非由于原始页面的删除而缩小为零，这是我们的金丝雀条件。（为了非常精确，我们比这更严格，因为它可能只是目标页面拆分，只有原始目标页面被删除。我们可以更严格，只是不能更宽松。）
	 *
	 * 顶层树遍历调用者在恢复此竞争后移动到下一个页面（使其成为新的目标）。 （参见 bt_child_check() 中需要 ShareLock 的子/下链验证的理由，在这里页面删除也是主要麻烦源。）
	 *
	 * 请注意，这里右兄弟页面是否实际上是表亲页面无关紧要，因为为了使键空间以导致上一级出现问题的方式进行重调整（将有问题的并发插入引导到表亲而不是父级的兄弟），必须对目标的父页面进行页面删除（影响目标在其祖父页面中父页面的下链）。仅当没有子页面时，内部页面删除才会发生（它们都已完全删除），并且调用者正在检查目标的父页面是否至少具有一个未删除（因此不被忽略）的子页面：目标页面。 （请注意，删除的第一阶段原子地标记页面为半死/可忽略，同时移除其父页面中的下链，因此调用者肯定不会未能检测到发生了这种情况。）
	 *
	 * 这个技巧受到反向扫描处理并发页面拆分方法的启发；并发页面删除是一个类似地有时会受到特殊考虑的问题（可能是反向扫描在未找到指向其的右链接后会重新读取其“原始”块，在已经多次向相反方向（右/“向前”）移动以尝试定位它）。就像我们一样，这仅发生在确定是否存在对参考页面的并发页面删除，并且就像我们一样，如果该引用页面被删除，这意味着我们可以继续关心引用页面。有关其工作原理的完整描述，请参见 nbtree README。
	 */
	fc_nline = PageGetMaxOffsetNumber(fc_rightpage);

	/*
	 * 获取第一个数据项（如果有的话）
	 */
	if (P_ISLEAF(fc_opaque) && fc_nline >= P_FIRSTDATAKEY(fc_opaque))
	{
		/* 返回第一个数据项（如果有的话） */
		fc_rightitem = fc_PageGetItemIdCareful(fc_state, fc_targetnext, fc_rightpage,
										 P_FIRSTDATAKEY(fc_opaque));
	}
	else if (!P_ISLEAF(fc_opaque) &&
			 fc_nline >= OffsetNumberNext(P_FIRSTDATAKEY(fc_opaque)))
	{
		/*
		 * 返回内部页的“负无穷”项之后的第一个项
		 */
		fc_rightitem = fc_PageGetItemIdCareful(fc_state, fc_targetnext, fc_rightpage,
										 OffsetNumberNext(P_FIRSTDATAKEY(fc_opaque)));
	}
	else
	{
		/*
		 * 没有第一个项。页面可能是空的叶子页面，但也有可能
		 * 是只有负无穷项的内部页面。
		 */
		ereport(DEBUG2,
				(errcode(ERRCODE_NO_DATA),
				 errmsg_internal("%s block %u of index \"%s\" has no first data item",
								 P_ISLEAF(fc_opaque) ? "leaf" : "internal", fc_targetnext,
								 RelationGetRelationName(fc_state->rel))));
		return NULL;
	}

	/*
	 * 返回第一个真实项的scankey。注意这依赖于右页
	 * 内存仍然被分配。
	 */
	fc_firstitup = (IndexTuple) PageGetItem(fc_rightpage, fc_rightitem);
	return fc_bt_mkscankey_pivotsearch(fc_state->rel, fc_firstitup);
}

/*
 * 检查两个元组在块编号之外是否是二进制相同的。因此，
 * 此函数能够比较不同级别上的枢轴键。
 */
static bool fc_bt_pivot_tuple_identical(bool fc_heapkeyspace, IndexTuple fc_itup1, IndexTuple fc_itup2)
{
	if (IndexTupleSize(fc_itup1) != IndexTupleSize(fc_itup2))
		return false;

	if (fc_heapkeyspace)
	{
		/*
		 * 偏移编号将包含堆键空间索引中的重要信息：
		 * 在后缀截断之后，枢轴元组中剩余的属性数量。
		 * 不要跳过它（也要比较它）。
		 */
		if (memcmp(&fc_itup1->t_tid.ip_posid, &fc_itup2->t_tid.ip_posid,
				   IndexTupleSize(fc_itup1) -
				   offsetof(ItemPointerData, ip_posid)) != 0)
			return false;
	}
	else
	{
		/*
		 * 不能依赖于偏移编号字段在pg_upgrade'd !heapkeyspace索引级别之间具有一致的值。
		 * 比较元组的内容，从项指针之后（即在块
		 * 编号和偏移编号之后）开始。
		 */
		if (memcmp(&fc_itup1->t_info, &fc_itup2->t_info,
				   IndexTupleSize(fc_itup1) -
				   offsetof(IndexTupleData, t_info)) != 0)
			return false;
	}

	return true;
}

/*---
 * 检查子级的高键。从之前的下链遍历右链到当前下链。
 * 检查是否没有缺失下链的中间页面。
 *
 * 如果给定了'loaded_child'，则假定它是指向目标页面的
 * 'downlinkoffnum'引用的下链所指向的页面。
 *
 * 基本上，这个函数为每个目标下链调用，检查两个不变量：
 *
 * 1) 可以通过右链从以前的子级到达下一个子级；
 * 2) 每个子级的高键在目标级别上有匹配的枢轴键。
 *
 * 考虑示例树图。
 *
 *               1
 *           /       \
 *        2     <->     3
 *      /   \        /     \
 *    4  <>  5  <> 6 <> 7 <> 8
 *
 * 这个函数将为块4、5、6和8调用。考虑每个函数调用发生的情况。
 *
 * - 对于块4的函数调用初始化数据结构并将块4的高键与
 *   块2的下链枢轴键匹配。
 * - 块5的高键与块2的高键匹配。
 * - 块6具有不完整的分割标志，因此其高键没有匹配任何内容。
 * - 对于块8的函数调用检查在从块6跟随右链时是否能找到块8。
 *   块7的高键将与块3中的下链枢轴键匹配。
 *
 * 此函数还有一个最终调用，检查在目标级别中右侧引用的子级
 * 的右侧没有缺失的下链。
 */
static void fc_bt_child_highkey_check(BtreeCheckState *fc_state,
					   OffsetNumber fc_target_downlinkoffnum,
					   Page fc_loaded_child,
					   uint32 fc_target_level)
{
	BlockNumber fc_blkno = fc_state->prevrightlink;
	Page		fc_page;
	BTPageOpaque fc_opaque;
	bool		fc_rightsplit = fc_state->previncompletesplit;
	bool		fc_first = true;
	ItemId		fc_itemid;
	IndexTuple	fc_itup;
	BlockNumber fc_downlink;

	if (OffsetNumberIsValid(fc_target_downlinkoffnum))
	{
		fc_itemid = fc_PageGetItemIdCareful(fc_state, fc_state->targetblock,
									  fc_state->target, fc_target_downlinkoffnum);
		fc_itup = (IndexTuple) PageGetItem(fc_state->target, fc_itemid);
		fc_downlink = BTreeTupleGetDownLink(fc_itup);
	}
	else
	{
		fc_downlink = P_NONE;
	}

	/*
	 * 如果当前级别下方没有记住的右链接，我们将从最左侧页面开始。
	 * 我们无法跟随上一页面的右链接，因为没有上一页面。
	 * 但我们仍然可以匹配高键。
	 *
	 * 因此，我们像有上一页面引用当前子项一样初始化上面的循环变量。
	 * 我们还假设上一页面没有未完成分裂标志，这会要求我们为当前子项提供下链接。
	 * 这是正确的，因为该级别的最左侧页面应该始终有父级下链接。
	 */
	if (!BlockNumberIsValid(fc_blkno))
	{
		fc_blkno = fc_downlink;
		fc_rightsplit = false;
	}

	/* 在子级别向右移动 */
	while (true)
	{
		/*
		 * 我们是否遍历了整个树级别，这是对右侧右下链接的页面的检查？
		 */
		if (fc_blkno == P_NONE && fc_downlink == P_NONE)
		{
			fc_state->prevrightlink = InvalidBlockNumber;
			fc_state->previncompletesplit = false;
			return;
		}

		/* 我们是否遍历了整个树级别而没有找到下一个下链接？ */
		if (fc_blkno == P_NONE)
			ereport(ERROR,
					(errcode(ERRCODE_INDEX_CORRUPTED),
					 errmsg("can't traverse from downlink %u to downlink %u of index \"%s\"",
							fc_state->prevrightlink, fc_downlink,
							RelationGetRelationName(fc_state->rel))));

		/* 加载页面内容 */
		if (fc_blkno == fc_downlink && fc_loaded_child)
			fc_page = fc_loaded_child;
		else
			fc_page = fc_palloc_btree_page(fc_state, fc_blkno);

		fc_opaque = BTPageGetOpaque(fc_page);

		/* 我们在该级别访问的第一个页面应该是最左侧的 */
		if (fc_first && !BlockNumberIsValid(fc_state->prevrightlink) &&
			!fc_bt_leftmost_ignoring_half_dead(fc_state, fc_blkno, fc_opaque))
			ereport(ERROR,
					(errcode(ERRCODE_INDEX_CORRUPTED),
					 errmsg("the first child of leftmost target page is not leftmost of its level in index \"%s\"",
							RelationGetRelationName(fc_state->rel)),
					 errdetail_internal("Target block=%u child block=%u target page lsn=%X/%X.",
										fc_state->targetblock, fc_blkno,
										LSN_FORMAT_ARGS(fc_state->targetlsn))));

		/* 进行级别的完整性检查 */
		if ((!P_ISDELETED(fc_opaque) || P_HAS_FULLXID(fc_opaque)) &&
			fc_opaque->btpo_level != fc_target_level - 1)
			ereport(ERROR,
					(errcode(ERRCODE_INDEX_CORRUPTED),
					 errmsg("block found while following rightlinks from child of index \"%s\" has invalid level",
							RelationGetRelationName(fc_state->rel)),
					 errdetail_internal("Block pointed to=%u expected level=%u level in pointed to block=%u.",
										fc_blkno, fc_target_level - 1, fc_opaque->btpo_level)));

		/* 尝试检测循环链接 */
		if ((!fc_first && fc_blkno == fc_state->prevrightlink) || fc_blkno == fc_opaque->btpo_prev)
			ereport(ERROR,
					(errcode(ERRCODE_INDEX_CORRUPTED),
					 errmsg("circular link chain found in block %u of index \"%s\"",
							fc_blkno, RelationGetRelationName(fc_state->rel))));

		if (fc_blkno != fc_downlink && !P_IGNORE(fc_opaque))
		{
			/* blkno 可能缺少父级下链接 */
			fc_bt_downlink_missing_check(fc_state, fc_rightsplit, fc_blkno, fc_page);
		}

		fc_rightsplit = P_INCOMPLETE_SPLIT(fc_opaque);

		/*
		 * 如果我们访问的页面具有高键，请检查它是否等于
		 * 对应下链接旁边的目标键。
		 */
		if (!fc_rightsplit && !P_RIGHTMOST(fc_opaque))
		{
			BTPageOpaque fc_topaque;
			IndexTuple	fc_highkey;
			OffsetNumber fc_pivotkey_offset;

			/* 获取高键 */
			fc_itemid = fc_PageGetItemIdCareful(fc_state, fc_blkno, fc_page, P_HIKEY);
			fc_highkey = (IndexTuple) PageGetItem(fc_page, fc_itemid);

			/*
			 * 当我们检查高键时，可能会有两种情况。
			 * 如果当前子页面由给定目标下链接引用，我们
			 * 应该查看下一个偏移号以匹配目标页面的键。
			 *
			 * 或者，我们正在跟随右链接，在上一个目标下链接引用的页面和
			 * 当前目标下链接引用的页面之间的某个中间位置。
			 * 如果当前子页面没有设置未完成分裂标志，那么它的
			 * 高键应该与当前偏移号的目标键匹配。
			 * 当之前的调用（到bt_child_highkey_check()）发现一个未完成的分裂时，会发生这种情况，
			 * 我们到达一个没有下链接的右兄弟页面——右兄弟页面的高键仍然需要匹配
			 * 父级/目标级别上的分隔键。
			 *
			 * 当我们已经在子级别向右移动时，不要对 target_downlinkoffnum 应用 OffsetNumberNext()。
			 * 我们在子级别的遍历必须试图与目标/父级级别的遍历保持完美的步伐（在其左侧）。
			 */
			if (fc_blkno == fc_downlink)
				fc_pivotkey_offset = OffsetNumberNext(fc_target_downlinkoffnum);
			else
				fc_pivotkey_offset = fc_target_downlinkoffnum;

			fc_topaque = BTPageGetOpaque(fc_state->target);

			if (!fc_offset_is_negative_infinity(fc_topaque, fc_pivotkey_offset))
			{
				/*
				 * 如果我们正在寻找目标页面中的下一个枢轴元组，
				 * 但没有更多的枢轴元组，那么我们应该匹配高键。
				 */
				if (fc_pivotkey_offset > PageGetMaxOffsetNumber(fc_state->target))
				{
					if (P_RIGHTMOST(fc_topaque))
						ereport(ERROR,
								(errcode(ERRCODE_INDEX_CORRUPTED),
								 errmsg("child high key is greater than rightmost pivot key on target level in index \"%s\"",
										RelationGetRelationName(fc_state->rel)),
								 errdetail_internal("Target block=%u child block=%u target page lsn=%X/%X.",
													fc_state->targetblock, fc_blkno,
													LSN_FORMAT_ARGS(fc_state->targetlsn))));
					fc_pivotkey_offset = P_HIKEY;
				}
				fc_itemid = fc_PageGetItemIdCareful(fc_state, fc_state->targetblock,
											  fc_state->target, fc_pivotkey_offset);
				fc_itup = (IndexTuple) PageGetItem(fc_state->target, fc_itemid);
			}
			else
			{
				/*
				 * 我们无法将孩子的高键与目标中的负无穷大键进行匹配，因为没有可以比较的东西。
				 * 然而，仍然可以在目标页面外部匹配孩子的高键。我们之所以这样做是因为
				 * bt_child_highkey_check() 之前已针对 'loaded_child' 的表亲页面调用过，该页面是未完整分裂。
				 * 因此，现在我们遍历该表亲页面的右侧，而当前考虑的孩子级别页面仍然属于
				 * 目标左兄弟的子树。因此，我们需要将孩子的高键与其左叔叔页面的高键匹配。
				 * 感谢我们保存了它，它被称为目标页面的“低键”。
				 */
				if (!fc_state->lowkey)
					ereport(ERROR,
							(errcode(ERRCODE_INDEX_CORRUPTED),
							 errmsg("can't find left sibling high key in index \"%s\"",
									RelationGetRelationName(fc_state->rel)),
							 errdetail_internal("Target block=%u child block=%u target page lsn=%X/%X.",
												fc_state->targetblock, fc_blkno,
												LSN_FORMAT_ARGS(fc_state->targetlsn))));
				fc_itup = fc_state->lowkey;
			}

			if (!fc_bt_pivot_tuple_identical(fc_state->heapkeyspace, fc_highkey, fc_itup))
			{
				ereport(ERROR,
						(errcode(ERRCODE_INDEX_CORRUPTED),
						 errmsg("mismatch between parent key and child high key in index \"%s\"",
								RelationGetRelationName(fc_state->rel)),
						 errdetail_internal("Target block=%u child block=%u target page lsn=%X/%X.",
											fc_state->targetblock, fc_blkno,
											LSN_FORMAT_ARGS(fc_state->targetlsn))));
			}
		}

		/* 如果我们已经找到下一个下行链接，则退出 */
		if (fc_blkno == fc_downlink)
		{
			fc_state->prevrightlink = fc_opaque->btpo_next;
			fc_state->previncompletesplit = fc_rightsplit;
			return;
		}

		/* 使用 rightlink 遍历到下一页面 */
		fc_blkno = fc_opaque->btpo_next;

		/* 如果页面是我们分配的，则释放页面内容 */
		if (fc_page != fc_loaded_child)
			pfree(fc_page);
		fc_first = false;
	}
}

/*
 * 检查目标的一个下行链接与其子页面的关系。
 *
 * 从概念上讲，目标页面仍然是这里检查的内容。发生不变性违反时，目标块仍然受到责备。
 * 向目标插入下行链接可能是这里出现任何问题的地方，并且不存在父链接，因此以这种方式进行验证
 * 更加实用。
 *
 * 此函数访问子页面，并针对目标页面的每个下行链接依次调用。
 * 假设这一点，我们还在这里检查下行链接的连通性，以便节省对子页面的访问。
 */
static void fc_bt_child_check(BtreeCheckState *fc_state, BTScanInsert fc_targetkey,
			   OffsetNumber fc_downlinkoffnum)
{
	ItemId		fc_itemid;
	IndexTuple	fc_itup;
	BlockNumber fc_childblock;
	OffsetNumber fc_offset;
	OffsetNumber fc_maxoffset;
	Page		fc_child;
	BTPageOpaque fc_copaque;
	BTPageOpaque fc_topaque;

	fc_itemid = fc_PageGetItemIdCareful(fc_state, fc_state->targetblock,
								  fc_state->target, fc_downlinkoffnum);
	fc_itup = (IndexTuple) PageGetItem(fc_state->target, fc_itemid);
	fc_childblock = BTreeTupleGetDownLink(fc_itup);

	/*
	 * 调用者必须在目标关系上持有 ShareLock，因为考虑到了 VACUUM 的页面删除。
	 *
	 * 注意：通常情况下，页面删除会删除右侧兄弟的下连接，而不是被删除页面的下连接；被删除页面的下连接会被其兄弟重用。这样，删除页面和其右侧兄弟之间的关键空间就得到了整合。（我们不能删除父页面的最右子项，除非它是最后一个子页面，并且我们打算同时删除父页面本身。）
	 *
	 * 如果这个验证是在没有 ShareLock 的情况下发生，以下竞争条件可能会导致假阳性：
	 *
	 * 一般来说，可能发生并发页面删除，包括删除这里检查的子页面的左侧兄弟。如果发生了这样的页面删除，紧接着在子页面的新扩展关键空间中插入一个项，就会出现假阳性的窗口：原本合法地跟随到子页面的过时父/目标下连接不再是页面中所有项的下限，因为关键空间被并发地“向左”扩展。（插入跟随的是子页面的“新”下连接，而不是我们现在过时的下连接，后者在删除的第一阶段作为目标/父的一部分被并发物理移除。）
	 *
	 * 虽然我们在其他地方使用各种技术来执行非只读调用者的跨页面验证，但在这里似乎很难使用类似的技巧。bt_recheck_sibling_links 和 bt_right_page_check_scankey 使用的技巧都涉及验证同级、跨兄弟的不变性。然而，跨层不变性要脆弱得多。nbtree REDO 例程在页面分裂期间实际上并不在不同层级之间耦合缓冲区锁，因此在非只读模式下使任何跨层检查可靠工作可能是不可能的。
	 */
	Assert(fc_state->readonly);

	/*
	 * 验证子页面具有来自目标页面（其父页面）的下连接键作为下限；下连接必须严格小于页面上的所有键。
	 *
	 * 检查所有项，而不是仅检查第一个并相信操作符类遵循传递法则。
	 */
	fc_topaque = BTPageGetOpaque(fc_state->target);
	fc_child = fc_palloc_btree_page(fc_state, fc_childblock);
	fc_copaque = BTPageGetOpaque(fc_child);
	fc_maxoffset = PageGetMaxOffsetNumber(fc_child);

	/*
	 * 由于我们已经加载了子块，将此检查与下连接连通性检查结合起来。
	 */
	fc_bt_child_highkey_check(fc_state, fc_downlinkoffnum,
						   fc_child, fc_topaque->btpo_level);

	/*
	 * 由于在只读模式下不能同时进行 VACUUM 操作，
	 * 而且一旦页面被标记为完全删除，它在其他页面（兄弟和父级）中没有链接，
	 * 因此应当不可能落在一个完全删除的页面上。
	 *
	 * 强制要求页面不能甚至半死也没有意义，尽管下游链接
	 * 在子叶页面被标记为半死的同一阶段被修改。这是错误的，因为
	 * 在删除过程中可能偶尔会有多个下游链接来自一系列页面，
	 * 它们在 VACUUM 之前对 _bt_unlink_halfdead_page() 的多次连续调用，
	 * 最终才能安全地将叶页面标记为完全死亡。尽管 _bt_mark_page_halfdead()
	 * 通常会移除指向被标记为半死的叶页面的下游链接，但这并不能保证，
	 * 所以可能我们会因为中断的多级页面删除而落在具有下游链接的半死页面上。
	 *
	 * 如果子页面是半死的，我们会继续进行检查。这样做是安全的，
	 * 因为我们不测试子页面的高键，因此原始高键被 _bt_mark_page_halfdead()
	 * 中的虚拟截断高键替代并不重要。半死页面上的其他页面项都保持不变，
	 * 所以仍然有一些东西可以测试。
	 */
	if (P_ISDELETED(fc_copaque))
		ereport(ERROR,
				(errcode(ERRCODE_INDEX_CORRUPTED),
				 errmsg("downlink to deleted page found in index \"%s\"",
						RelationGetRelationName(fc_state->rel)),
				 errdetail_internal("Parent block=%u child block=%u parent page lsn=%X/%X.",
									fc_state->targetblock, fc_childblock,
									LSN_FORMAT_ARGS(fc_state->targetlsn))));

	for (fc_offset = P_FIRSTDATAKEY(fc_copaque);
		 fc_offset <= fc_maxoffset;
		 fc_offset = OffsetNumberNext(fc_offset))
	{
		/*
		 * 如果存在，则跳过目标页面键与“负无穷”项的比较。
		 * 检查它会表明它不是一个严格的下限，但这仅仅是因为
		 * 在 _bt_compare() 中对负无穷项的硬编码。
		 *
		 * 如果 nbtree 在内部页面拆分期间没有截断负无穷元组，
		 * 那么我们期望子页面的负无穷键与目标/父级的扫描键/下游链接相等
		 * （在这个假设场景中，它将是一个“低键”，因此仍然需要
		 * 在这里作为特殊情况处理）。
		 *
		 * 负无穷项可以被视为一个严格的下界，具有传递性，
		 * 在从根节点下降时跟随的最后一个非负无穷支点
		 * 被视为它的“真实”严格下限。只有少数负无穷项是真正的
		 * 负无穷；那些是最左侧内部页面的第一个项。
		 * 更一般地说，负无穷项仅在页面所在的子树中是负无穷。
		 *
		 * 另见：bt_rootdescend()，它甚至可以检测到表亲叶页面上的
		 * 传递性不一致性。
		 */
		if (fc_offset_is_negative_infinity(fc_copaque, fc_offset))
			continue;

		if (!fc_invariant_l_nontarget_offset(fc_state, fc_targetkey, fc_childblock, fc_child,
										  fc_offset))
			ereport(ERROR,
					(errcode(ERRCODE_INDEX_CORRUPTED),
					 errmsg("down-link lower bound invariant violated for index \"%s\"",
							RelationGetRelationName(fc_state->rel)),
					 errdetail_internal("Parent block=%u child index tid=(%u,%u) parent page lsn=%X/%X.",
										fc_state->targetblock, fc_childblock, fc_offset,
										LSN_FORMAT_ARGS(fc_state->targetlsn))));
	}

	pfree(fc_child);
}

/*
 * 检查页面是否缺少应该存在的下链接。
 *
 * 一个缺乏下链接/父链接的页面可能表示数据损坏。然而，我们
 * 必须考虑到缺失下链接的情况偶尔会在非损坏的索引中出现。
 * 这可能是由于页面拆分被中断，或多级页面删除被中断（即在
 * 页面拆分期间或在VACUUM删除多级页面链条期间发生了硬崩溃
 * 或错误）。
 *
 * 请注意，这只能在只读模式下调用，因此无需担心并发页面
 * 拆分或页面删除。
 */
static void fc_bt_downlink_missing_check(BtreeCheckState *fc_state, bool fc_rightsplit,
						  BlockNumber fc_blkno, Page fc_page)
{
	BTPageOpaque fc_opaque = BTPageGetOpaque(fc_page);
	ItemId		fc_itemid;
	IndexTuple	fc_itup;
	Page		fc_child;
	BTPageOpaque fc_copaque;
	uint32		fc_level;
	BlockNumber fc_childblk;
	XLogRecPtr	fc_pagelsn;

	Assert(fc_state->readonly);
	Assert(!P_IGNORE(fc_opaque));

	/* 从真根没有下链接的下一级 */
	if (P_ISROOT(fc_opaque))
		return;

	fc_pagelsn = PageGetLSN(fc_page);

	/*
	 * 不完整（中断的）页面拆分可能导致缺少下链接。
	 * 某个插入事务最终应该能够完成页面拆分，
	 * 当它注意到左侧兄弟页面是 P_INCOMPLETE_SPLIT() 时。
	 *
	 * 一般来说，VACUUM 并未准备好处理其删除的页面没有下链接。
	 * 这就是在这里报告缺少下链接可能表示损坏的主要原因。
	 * 虽然无效的缺失下链接可能导致查询错误结果并不明显，
	 * 因为落在子页面上的索引扫描可能会持续向右移动。
	 * 在 _bt_moveright() 中处理并发页面拆分（和页面删除）无法
	 * 区分瞬时不一致和永久无法恢复的不一致。
	 *
	 * VACUUM 甚至没有准备删除由于不完整页面拆分而没有下链接的页面，
	 * 但它可以通过设计检测和推理这种情况，因此不应被视为
	 * 表示损坏。有关详细信息，请参见 _bt_pagedel()。
	 */
	if (fc_rightsplit)
	{
		ereport(DEBUG1,
				(errcode(ERRCODE_NO_DATA),
				 errmsg_internal("harmless interrupted page split detected in index \"%s\"",
								 RelationGetRelationName(fc_state->rel)),
				 errdetail_internal("Block=%u level=%u left sibling=%u page lsn=%X/%X.",
									fc_blkno, fc_opaque->btpo_level,
									fc_opaque->btpo_prev,
									LSN_FORMAT_ARGS(fc_pagelsn))));
		return;
	}

	/*
	 * 待检查的页面可能是多级页面删除的“顶级父页面”。
	 * 不过，我们需要向下遍历子树，以确保子页面与之
	 * 一致。
	 *
	 * 如果该页面（必须不可忽略）是叶子页面，那么显然
	 * 它不可能是顶级父页面。缺少下链接可能是广泛问题的
	 * 征兆，这个问题同样可能在其他地方造成不一致。
	 */
	if (P_ISLEAF(fc_opaque))
		ereport(ERROR,
				(errcode(ERRCODE_INDEX_CORRUPTED),
				 errmsg("leaf index block lacks downlink in index \"%s\"",
						RelationGetRelationName(fc_state->rel)),
				 errdetail_internal("Block=%u page lsn=%X/%X.",
									fc_blkno,
									LSN_FORMAT_ARGS(fc_pagelsn))));

	/* 从给定页面向下遍历，它是一个内部页面 */
	elog(DEBUG1, "checking for interrupted multi-level deletion due to missing downlink in index \"%s\"",
		 RelationGetRelationName(fc_state->rel));

	fc_level = fc_opaque->btpo_level;
	fc_itemid = fc_PageGetItemIdCareful(fc_state, fc_blkno, fc_page, P_FIRSTDATAKEY(fc_opaque));
	fc_itup = (IndexTuple) PageGetItem(fc_page, fc_itemid);
	fc_childblk = BTreeTupleGetDownLink(fc_itup);
	for (;;)
	{
		CHECK_FOR_INTERRUPTS();

		fc_child = fc_palloc_btree_page(fc_state, fc_childblk);
		fc_copaque = BTPageGetOpaque(fc_child);

		if (P_ISLEAF(fc_copaque))
			break;

		/* 在通过时对内部页面进行额外的合理性检查 */
		if (fc_copaque->btpo_level != fc_level - 1)
			ereport(ERROR,
					(errcode(ERRCODE_INDEX_CORRUPTED),
					 errmsg_internal("downlink points to block in index \"%s\" whose level is not one level down",
									 RelationGetRelationName(fc_state->rel)),
					 errdetail_internal("Top parent/under check block=%u block pointed to=%u expected level=%u level in pointed to block=%u.",
										fc_blkno, fc_childblk,
										fc_level - 1, fc_copaque->btpo_level)));

		fc_level = fc_copaque->btpo_level;
		fc_itemid = fc_PageGetItemIdCareful(fc_state, fc_childblk, fc_child,
									  P_FIRSTDATAKEY(fc_copaque));
		fc_itup = (IndexTuple) PageGetItem(fc_child, fc_itemid);
		fc_childblk = BTreeTupleGetDownLink(fc_itup);
		/* 稍微积极一些以释放这块内存，以防万一 */
		pfree(fc_child);
	}

	/*
	 * 因为在只读模式下不能有并发的VACUUM操作，
	 * 并且一旦页面被标记为完全删除，它在其他页面（兄弟和父页面）中没有链接，
	 * 因此应该不可能访问到一个完全删除的页面。有关详细信息，请参见bt_child_check()。
	 *
	 * 这里重复了bt_child_check() P_ISDELETED()检查，因为
	 * bt_child_check()不会访问通过负无穷项可达的页面。此外，bt_child_check()不愿意
	 * 降级多个层次。（bt_check_level_from_leftmost()中的类似bt_child_check() P_ISDELETED()检查
	 * 也无法到达该页面，因为叶子的活兄弟应该更新它们的兄弟链接，以绕过当被标记为完全死亡的删除目标页面。）
	 *
	 * 如果引发此错误，可能是由于之前的多级页面删除未能意识到
	 * 在安全标记叶页面为完全死亡之前，它还不安全。当发生这种情况时，
	 * 仍然会留下“悬空下行链接”。悬空下行链接的页面（叶子的
	 * 父页面/祖先页面）缺少下行链接是偶然的。
	 */
	if (P_ISDELETED(fc_copaque))
		ereport(ERROR,
				(errcode(ERRCODE_INDEX_CORRUPTED),
				 errmsg_internal("downlink to deleted leaf page found in index \"%s\"",
								 RelationGetRelationName(fc_state->rel)),
				 errdetail_internal("Top parent/target block=%u leaf block=%u top parent/under check lsn=%X/%X.",
									fc_blkno, fc_childblk,
									LSN_FORMAT_ARGS(fc_pagelsn))));

	/*
	 * 仅当叶页面半死时，它的高键顶层父链接应指向
	 * VACUUM考虑到在被打断时的顶层父页面。假如高键链接确实指向
	 * 被检查的页面，则我们检测到的缺失下行链接与
	 * 此前发生的中断的多级页面删除是一致的。这意味着
	 * 以被检查页面为根的子树（页面删除链）处于一致状态，
	 * 使得VACUUM能够在下次遇到半死的叶页面时恢复删除整个链。
	 */
	if (P_ISHALFDEAD(fc_copaque) && !P_RIGHTMOST(fc_copaque))
	{
		fc_itemid = fc_PageGetItemIdCareful(fc_state, fc_childblk, fc_child, P_HIKEY);
		fc_itup = (IndexTuple) PageGetItem(fc_child, fc_itemid);
		if (BTreeTupleGetTopParent(fc_itup) == fc_blkno)
			return;
	}

	ereport(ERROR,
			(errcode(ERRCODE_INDEX_CORRUPTED),
			 errmsg("internal index block lacks downlink in index \"%s\"",
					RelationGetRelationName(fc_state->rel)),
			 errdetail_internal("Block=%u level=%u page lsn=%X/%X.",
								fc_blkno, fc_opaque->btpo_level,
								LSN_FORMAT_ARGS(fc_pagelsn))));
}

/*
 * 来自 table_index_build_scan 的每元组回调，用于确定索引是否具有
 * 所有应在目标索引的叶页面中观察到的条目（即，所有被我们的
 * Bloom filter 指纹识别的 IndexTuples）。所有 heapallindexed 检查
 * 都在这里进行。
 *
 * 索引与其索引的表之间的冗余提供了一个很好
 * 的机会来检测损坏，特别是表内的损坏。
 * 在这里执行的验证背后的高层原则是，任何
 * 在基于相同索引定义的全新 CREATE INDEX 后应该在索引中的
 * IndexTuple，也应该在原始的、现有的索引中，这两个索引应该
 * 使用完全相同的表示方式。
 *
 * 由于索引的整体结构已经得到验证，这里错误的最
 * 可能解释是损坏的堆页面（可能是逻辑
 * 或物理损坏）。不过，索引损坏仍然可能在这里被检测到。
 * 只有只读调用者将验证左链接和右链接的一致性，
 * 所以在索引中的叶页面转置实际上可能是这里检测到的
 * 损坏源（对于非只读调用者）。仅针对只读调用者执行的检查
 * 可能更准确地将问题框定为跨页面不变性问题（这甚至可能是
 * 由于恢复未重放所有 WAL 记录造成的）。这里抛出的非只读
 * ERROR 消息包含有关重新尝试只读验证的提示，以防这是跨页面
 * 不变性问题，尽管这并不是特别可能。
 *
 * table_index_build_scan() 期望能够找到根元组，当需要索引
 * 一个堆仅元组（某个 HOT 链末尾的活元组）时，以便将实际
 * 元组的 TID 替换为根元组的 TID（这正是我们在这里实际返回的）。
 * 当声称是堆仅元组的 HOT 链根的元组无法找到时，索引构建堆
 * 扫描代码将抛出错误。这捕获了这样的情况，即HOT链的原始根
 * 项偏移量/根元组表示（出于某种原因）整个 HOT 链已经死亡，尽管
 * 最新的堆仅元组应该被索引。发生此情况时，顺序扫描可能总是
 * 给出正确的答案，所有索引可能被视为结构上
 * 一致（即，nbtree 结构检查不会检测到损坏）。可能仅有索引
 * 扫描给出错误答案，而堆或 SLRU损坏是真正的罪魁祸首。
 * （虽然 LP_DEAD 位设置可能也会在不久之后使索引处于损坏状态，
 * 但问题仍然是确实存在堆损坏。）
 *
 * 在 table_index_build_scan() 中处理堆仅元组的方式
 * 帮助我们检测包含错误值的索引元组（与 HOT 链中最新元组不匹配的值）。
 * 当由于对 HOT 安全性的错误评估没有替代索引元组时，这种情况可能发生，
 * 也许是在最初的 CREATE INDEX 期间。
 * 因为最新元组的内容与根 TID 一起使用，当一个具有相同 TID
 * 但属性值不匹配的元组返回给我们时，将抛出错误。
 * 对 HOT 安全性错误评估是导致至少两个不同的 CREATE INDEX
 * CONCURRENTLY 错误的原因，这些错误已进入稳定版本，其中一个
 * 在许多年内没有被检测到。简而言之，允许 REINDEX 在存在
 * （未检测到的）损坏的 HOT 链时修复损坏的相同原则，
 * 也使我们能够在许多情况下检测到损坏。
 */
static void fc_bt_tuple_present_callback(Relation fc_index, ItemPointer fc_tid, Datum *fc_values,
						  bool *fc_isnull, bool fc_tupleIsAlive, void *fc_checkstate)
{
	BtreeCheckState *fc_state = (BtreeCheckState *) fc_checkstate;
	IndexTuple	fc_itup,
				fc_norm;

	Assert(fc_state->heapallindexed);

	/* 生成用于指纹识别的标准化索引元组 */
	fc_itup = index_form_tuple(RelationGetDescr(fc_index), fc_values, fc_isnull);
	fc_itup->t_tid = *fc_tid;
	fc_norm = fc_bt_normalize_tuple(fc_state, fc_itup);

	/* 探测布隆过滤器 -- 元组应该存在 */
	if (bloom_lacks_element(fc_state->filter, (unsigned char *) fc_norm,
							IndexTupleSize(fc_norm)))
		ereport(ERROR,
				(errcode(ERRCODE_DATA_CORRUPTED),
				 errmsg("heap tuple (%u,%u) from table \"%s\" lacks matching index tuple within index \"%s\"",
						ItemPointerGetBlockNumber(&(fc_itup->t_tid)),
						ItemPointerGetOffsetNumber(&(fc_itup->t_tid)),
						RelationGetRelationName(fc_state->heaprel),
						RelationGetRelationName(fc_state->rel)),
				 !fc_state->readonly
				 ? errhint("Retrying verification using the function bt_index_parent_check() might provide a more specific error.")
				 : 0));

	fc_state->heaptuplespresent++;
	pfree(fc_itup);
	/* 在这里不能泄漏内存 */
	if (fc_norm != fc_itup)
		pfree(fc_norm);
}

/*
 * 使索引元组标准化，以便进行指纹识别。
 *
 * 通常，索引元组的形成被假设为确定性的，由
 * heapallindexed 验证，而 IndexTuples 被假设为不可变的。虽然
 * LP_DEAD 位在叶页中是可变的，但那是 ItemId 元数据，
 * 不会被指纹识别。标准化是为了补偿那些
 * 确定性假设不完全成立的边缘情况。
 *
 * 当前有一个这样的情况：index_form_tuple() 不尝试隐藏
 * 输入数据项的源 TOAST 状态。执行器基于不同的标准
 * 对堆元组应用 TOAST 压缩，这与 btinsert() 调用
 * index_form_tuple() 中的压缩应用有所不同：它有时会
 * 更积极地进行压缩，导致压缩的堆元组数据项但
 * 对应的索引元组数据项未被压缩。后续的 heapallindexed
 * 验证将从 index_form_tuple() 获取一个在逻辑上等价但按位
 * 不相等的元组。没有标准化掉不一致性可能会导致
 * 虚假正面的 heapallindexed 损坏报告。
 *
 * 返回的元组通常是调用者自己的原始元组。否则，它是一个
 * 调用者原始索引元组的新表示，palloc() 在调用者的
 * 内存上下文中分配。
 *
 * 注意：此例程不关心元组表示的区别，超出了可能破坏
 * heapallindexed 验证的范围。特别是，它不会尝试对
 * 具有潜在不同表示的 opclass 相等数据项进行标准化
 *（例如，btree/numeric_ops 索引数据项的显示比例不会
 * 在这里被标准化）。调用者对具有发布列表的非支点
 * 元组进行标准化，因为虚拟的 CREATE INDEX 回调代码
 * 生成具有相同标准化表示的新元组。
 */
static IndexTuple fc_bt_normalize_tuple(BtreeCheckState *fc_state, IndexTuple fc_itup)
{
	TupleDesc	fc_tupleDescriptor = RelationGetDescr(fc_state->rel);
	Datum		fc_normalized[INDEX_MAX_KEYS];
	bool		fc_isnull[INDEX_MAX_KEYS];
	bool		fc_need_free[INDEX_MAX_KEYS];
	bool		fc_formnewtup = false;
	IndexTuple	fc_reformed;
	int			fc_i;

	/* 调用者在这里只能传递“逻辑上”非支点元组 */
	Assert(!BTreeTupleIsPosting(fc_itup) && !BTreeTupleIsPivot(fc_itup));

	/* 简单案例：很明显元组没有 varlena 数据项 */
	if (!IndexTupleHasVarwidths(fc_itup))
		return fc_itup;

	for (fc_i = 0; fc_i < fc_tupleDescriptor->natts; fc_i++)
	{
		Form_pg_attribute fc_att;

		fc_att = TupleDescAttr(fc_tupleDescriptor, fc_i);

		/* 初始假设为未烘烤/已经标准化的数据项 */
		fc_need_free[fc_i] = false;
		fc_normalized[fc_i] = index_getattr(fc_itup, fc_att->attnum,
									  fc_tupleDescriptor,
									  &fc_isnull[fc_i]);
		if (fc_att->attbyval || fc_att->attlen != -1 || fc_isnull[fc_i])
			continue;

		/*
		 * 调用者总是传递一个可以安全插入到
		 * 索引中的元组，而无需进一步处理，因此
		 * 在这里永远不会遇到外部 varlena 头。
		 */
		if (VARATT_IS_EXTERNAL(DatumGetPointer(fc_normalized[fc_i])))
			ereport(ERROR,
					(errcode(ERRCODE_INDEX_CORRUPTED),
					 errmsg("external varlena datum in tuple that references heap row (%u,%u) in index \"%s\"",
							ItemPointerGetBlockNumber(&(fc_itup->t_tid)),
							ItemPointerGetOffsetNumber(&(fc_itup->t_tid)),
							RelationGetRelationName(fc_state->rel))));
		else if (!VARATT_IS_COMPRESSED(DatumGetPointer(fc_normalized[fc_i])) &&
				 VARSIZE(DatumGetPointer(fc_normalized[fc_i])) > TOAST_INDEX_TARGET &&
				 (fc_att->attstorage == TYPSTORAGE_EXTENDED ||
				  fc_att->attstorage == TYPSTORAGE_MAIN))
		{
			/*
			 * 该值将由 index_form_tuple() 根据
			 * 当前存储设置进行压缩。我们可能在这里，
			 * 因为该元组是使用不同的存储设置形成的。
			 * 所以，强制形成。
			 */
			fc_formnewtup = true;
		}
		else if (VARATT_IS_COMPRESSED(DatumGetPointer(fc_normalized[fc_i])))
		{
			fc_formnewtup = true;
			fc_normalized[fc_i] = PointerGetDatum(PG_DETOAST_DATUM(fc_normalized[fc_i]));
			fc_need_free[fc_i] = true;
		}

		/*
		 * 短元组可以有 1B 或 4B 的头。将短元组的 4B
		 * 头转换为 1B。
		 */
		else if (VARATT_CAN_MAKE_SHORT(DatumGetPointer(fc_normalized[fc_i])))
		{
			/* 转换为短 varlena */
			Size		fc_len = VARATT_CONVERTED_SHORT_SIZE(DatumGetPointer(fc_normalized[fc_i]));
			char	   *fc_data = palloc(fc_len);

			SET_VARSIZE_SHORT(fc_data, fc_len);
			memcpy(fc_data + 1, VARDATA(DatumGetPointer(fc_normalized[fc_i])), fc_len - 1);

			fc_formnewtup = true;
			fc_normalized[fc_i] = PointerGetDatum(fc_data);
			fc_need_free[fc_i] = true;
		}
	}

	/*
	 * 简单情况：元组具有 varlena 数据项，且这些数据项均未压缩或
	 * 具有 4B 头的短元组。
	 */
	if (!fc_formnewtup)
		return fc_itup;

	/*
	 * 难处理情况：元组包含已压缩的 varlena 数据，需要
	 * 从未压缩的输入数据（标准化的输入数据）创建元组的标准化版本。
	 * 这相当天真，但不应该太常见。
	 *
	 * 在堆中，元组可能包含带有 1B
	 * 头和 4B 头的短 varlena 数据。但相应的索引元组应该始终
	 * 具有带有 1B 头的 varlena。如果有短的带 4B 头的 varlena，
	 * 我们需要将其转换以进行指纹识别。
	 *
	 * 请注意，我们依赖于确定性的 index_form_tuple() TOAST 压缩
	 * 标准化输入。
	 */
	fc_reformed = index_form_tuple(fc_tupleDescriptor, fc_normalized, fc_isnull);
	fc_reformed->t_tid = fc_itup->t_tid;

	/* 在这里不能泄漏内存 */
	for (fc_i = 0; fc_i < fc_tupleDescriptor->natts; fc_i++)
		if (fc_need_free[fc_i])
			pfree(DatumGetPointer(fc_normalized[fc_i]));

	return fc_reformed;
}

/*
 * 为第 n 个发布列表条目/TID 生成 palloc() 的“普通”元组。
 *
 * 一般来说，去重不应该改变索引的逻辑内容。
 * 多个索引元组在方便时合并成一个等效的
 * 发布列表索引元组。
 *
 * heapallindexed 验证必须通过将发布列表元组转换为两个或多个“普通”
 * 元组来标准化这种表示的变化。每个元组必须单独进行指纹识别——在堆扫描
 * 过程中，每个对应的布隆过滤器探测都必须有一个元组。
 *
 * 注意：调用者仍然需要使用返回的元组调用 bt_normalize_tuple()。
 */
static inline IndexTuple fc_bt_posting_plain_tuple(IndexTuple fc_itup, int fc_n)
{
	Assert(BTreeTupleIsPosting(fc_itup));

	/* 返回非发布列表元组 */
	return _bt_form_posting(fc_itup, BTreeTupleGetPostingN(fc_itup, fc_n), 1);
}

/*
 * 从快速根页面开始在索引中搜索 itup。itup 必须是
 * 非支点元组。这仅在 heapkeyspace 索引中受到支持，因为
 * 我们依赖于拥有完全唯一的键来找到匹配项，仅需访问一次
 * 叶子页面，除非发生中断的页面分裂，届时我们可能需要
 * 向右移动。（并发页面分裂是不可能的，因为调用者必须
 * 是只读调用者。）
 *
 * 这个例程可以检测树的多个级别之间非常微妙的传递一致性问题。
 * 叶子页面都有一个高键（即使最右侧页面也具有概念上的正无穷大高键），
 * 但没有低键。它们在父节点中的下行链接是一个下界，结合高
 * 键几乎足以检测所有可能的不一致性。然而，来自父节点的下行链接
 * 分隔键值并不总是可用，因为内部页面的第一个项目是负无穷大项目，
 * 在内部页面分裂期间截断为零属性。虽然确实可以通过
 * bt_child_check() 和高键检查检测大多数可想象的键
 * 空间问题，但在堂兄叶子页面中的非支点元组仍然存在未检测到的问题。
 * 从根节点开始搜索每个现有的叶子元组可以检测树的上层
 * 中的小不一致性，而这些是不可能通过其他方式检测的。
 * （除此之外，这可能也是对索引扫描本身使用的代码的直接测试。）
 */
static bool fc_bt_rootdescend(BtreeCheckState *fc_state, IndexTuple fc_itup)
{
	BTScanInsert fc_key;
	BTStack		fc_stack;
	Buffer		fc_lbuf;
	bool		fc_exists;

	fc_key = _bt_mkscankey(fc_state->rel, fc_itup);
	Assert(fc_key->heapkeyspace && fc_key->scantid != NULL);

	/*
	 * 从根开始搜索。
	 *
	 * 理想情况下，我们会安排仅在_detected_ interrupted page split
	 * 发现时在_bt_search()内向右移动（即当找到不完整的分裂位被设置时），
	 * 但现在我们接受这种可能性，可能会掩盖不一致性。
	 */
	Assert(fc_state->readonly && fc_state->rootdescend);
	fc_exists = false;
	fc_stack = _bt_search(fc_state->rel, fc_key, &fc_lbuf, BT_READ, NULL);

	if (BufferIsValid(fc_lbuf))
	{
		BTInsertStateData fc_insertstate;
		OffsetNumber fc_offnum;
		Page		fc_page;

		fc_insertstate.itup = fc_itup;
		fc_insertstate.itemsz = MAXALIGN(IndexTupleSize(fc_itup));
		fc_insertstate.itup_key = fc_key;
		fc_insertstate.postingoff = 0;
		fc_insertstate.bounds_valid = false;
		fc_insertstate.buf = fc_lbuf;

		/* 获取叶页面上的匹配元组 */
		fc_offnum = _bt_binsrch_insert(fc_state->rel, &fc_insertstate);
		/* 比较叶页面上第一个>=匹配项（如果有） */
		fc_page = BufferGetPage(fc_lbuf);
		/* 当元组具有发布列表时，应在第一个堆TID上匹配 */
		if (fc_offnum <= PageGetMaxOffsetNumber(fc_page) &&
			fc_insertstate.postingoff <= 0 &&
			_bt_compare(fc_state->rel, fc_key, fc_page, fc_offnum) == 0)
			fc_exists = true;
		_bt_relbuf(fc_state->rel, fc_lbuf);
	}

	_bt_freestack(fc_stack);
	pfree(fc_key);

	return fc_exists;
}

/*
 * 页面中特定偏移量（其特殊状态由调用者传递）是负无穷项吗？
 *
 * 正如在_bt_compare()上面的注释中所述，第一数据项被特殊处理为
 * “负无穷”项。_bt_compare()中的硬编码使得为了验证而比较该项
 * 基本上是无意义的，因为IndexTuple仅包含一个有效的TID（指向子页面的参考TID）。
 */
static inline bool fc_offset_is_negative_infinity(BTPageOpaque fc_opaque, OffsetNumber fc_offset)
{
	/*
	 * 仅对内部页面而言，高键之后的第一个项（如果有）是负无穷项。
	 * 内部页面总是有负无穷项，而叶页面则从不有。这意味着负无穷
	 * 项要么是第一个要么是第二个行项目，或者页面内根本没有。
	 *
	 * 负无穷项是在中心元组中的特殊情况。它们总是具有零属性，而
	 * 所有其他中心元组总是具有nkeyatts属性。
	 *
	 * 最右侧的页面没有高键，但可以说在概念上具有“正无穷”高键。
	 * 因此，父页面中的向下链接项和子页面中的高键之间存在对称性。
	 * 它们共同表示属于每个索引页面的键空间部分。例如，所有根页面
	 * 的子页面将从根负无穷向下链接的下限是负无穷，上限是正无穷
	 * （隐式地来自根中“虚构的”正无穷高键）。
	 */
	return !P_ISLEAF(fc_opaque) && fc_offset == P_FIRSTDATAKEY(fc_opaque);
}

/*
 * 不变量是否保持，键严格小于给定的上限偏移量项？
 *
 * 代表调用者验证行指针。
 *
 * 如果该函数返回false，约定是调用者因损坏抛出错误。
 */
static inline bool fc_invariant_l_offset(BtreeCheckState *fc_state, BTScanInsert fc_key,
				   OffsetNumber fc_upperbound)
{
	ItemId		fc_itemid;
	int32		fc_cmp;

	Assert(fc_key->pivotsearch);

	/* 在检查元组之前验证行指针 */
	fc_itemid = fc_PageGetItemIdCareful(fc_state, fc_state->targetblock, fc_state->target,
								  fc_upperbound);
	/* pg_upgrade过的索引可能合法地具有相等的兄弟元组 */
	if (!fc_key->heapkeyspace)
		return fc_invariant_leq_offset(fc_state, fc_key, fc_upperbound);

	fc_cmp = _bt_compare(fc_state->rel, fc_key, fc_state->target, fc_upperbound);

	/*
	 * _bt_compare()能够确定带有已填充属性的scankey大于在截断属性
	 * 上解决的中心元组（中心元组中属性的值是负无穷）。然而，它无法确定
	 * 一个scankey在_scankey_负无穷属性的基础上“小于”一个元组。
	 * 完成额外的一步来模拟为省略的scankey属性具有负无穷值。
	 */
	if (fc_cmp == 0)
	{
		BTPageOpaque fc_topaque;
		IndexTuple	fc_ritup;
		int			fc_uppnkeyatts;
		ItemPointer fc_rheaptid;
		bool		fc_nonpivot;

		fc_ritup = (IndexTuple) PageGetItem(fc_state->target, fc_itemid);
		fc_topaque = BTPageGetOpaque(fc_state->target);
		fc_nonpivot = P_ISLEAF(fc_topaque) && fc_upperbound >= P_FIRSTDATAKEY(fc_topaque);

		/* 获取右侧项的键数 + 堆TID */
		fc_uppnkeyatts = BTreeTupleGetNKeyAtts(fc_ritup, fc_state->rel);
		fc_rheaptid = fc_BTreeTupleGetHeapTIDCareful(fc_state, fc_ritup, fc_nonpivot);

		/* Heap TID is tiebreaker key attribute */
		if (fc_key->keysz == fc_uppnkeyatts)
			return fc_key->scantid == NULL && fc_rheaptid != NULL;

		return fc_key->keysz < fc_uppnkeyatts;
	}

	return fc_cmp < 0;
}

/*
 * 不变性是否成立，即关键字小于或等于给定的上界偏移项？
 *
 * 调用者应该通过使用 PageGetItemIdCareful() 调用验证上界的行指针是一致的。
 *
 * 如果该函数返回 false，约定是调用者因损坏而抛出错误。
 */
static inline bool fc_invariant_leq_offset(BtreeCheckState *fc_state, BTScanInsert fc_key,
					 OffsetNumber fc_upperbound)
{
	int32		fc_cmp;

	Assert(fc_key->pivotsearch);

	fc_cmp = _bt_compare(fc_state->rel, fc_key, fc_state->target, fc_upperbound);

	return fc_cmp <= 0;
}

/*
 * 不变性是否成立，即关键字严格大于给定的下界偏移项？
 *
 * 调用者应该通过使用 PageGetItemIdCareful() 调用验证下界的行指针是一致的。
 *
 * 如果该函数返回 false，约定是调用者因损坏而抛出错误。
 */
static inline bool fc_invariant_g_offset(BtreeCheckState *fc_state, BTScanInsert fc_key,
				   OffsetNumber fc_lowerbound)
{
	int32		fc_cmp;

	Assert(fc_key->pivotsearch);

	fc_cmp = _bt_compare(fc_state->rel, fc_key, fc_state->target, fc_lowerbound);

	/* pg_upgrade过的索引可能合法地具有相等的兄弟元组 */
	if (!fc_key->heapkeyspace)
		return fc_cmp >= 0;

	/*
	 * 无需考虑 scankey 具有需要强制解释为负无穷的属性的可能性。_bt_compare() 能够判断 scankey 大于负无穷。这里“==”和“<”之间的区别并不重要，因为无论哪种情况都表明存在损坏。
	 */
	return fc_cmp > 0;
}

/*
 * 不变性是否成立，即关键字严格小于给定的上界偏移项，该偏移项与调用者提供的不是当前目标页的页相关？
 *
 * 调用者的非目标页是目标的子页，在检查目标页的属性（即，关键字来自目标）时作为一部分进行了检查。代表调用者验证行指针。
 *
 * 如果该函数返回 false，约定是调用者因损坏而抛出错误。
 */
static inline bool fc_invariant_l_nontarget_offset(BtreeCheckState *fc_state, BTScanInsert fc_key,
							 BlockNumber fc_nontargetblock, Page fc_nontarget,
							 OffsetNumber fc_upperbound)
{
	ItemId		fc_itemid;
	int32		fc_cmp;

	Assert(fc_key->pivotsearch);

	/* 在检查元组之前验证行指针 */
	fc_itemid = fc_PageGetItemIdCareful(fc_state, fc_nontargetblock, fc_nontarget,
								  fc_upperbound);
	fc_cmp = _bt_compare(fc_state->rel, fc_key, fc_nontarget, fc_upperbound);

	/* pg_upgrade过的索引可能合法地具有相等的兄弟元组 */
	if (!fc_key->heapkeyspace)
		return fc_cmp <= 0;

	/* 有关此额外步骤的解释，请参见 invariant_l_offset() */
	if (fc_cmp == 0)
	{
		IndexTuple	fc_child;
		int			fc_uppnkeyatts;
		ItemPointer fc_childheaptid;
		BTPageOpaque fc_copaque;
		bool		fc_nonpivot;

		fc_child = (IndexTuple) PageGetItem(fc_nontarget, fc_itemid);
		fc_copaque = BTPageGetOpaque(fc_nontarget);
		fc_nonpivot = P_ISLEAF(fc_copaque) && fc_upperbound >= P_FIRSTDATAKEY(fc_copaque);

		/* 获取子项/非目标项的键数量 + heap TID */
		fc_uppnkeyatts = BTreeTupleGetNKeyAtts(fc_child, fc_state->rel);
		fc_childheaptid = fc_BTreeTupleGetHeapTIDCareful(fc_state, fc_child, fc_nonpivot);

		/* Heap TID is tiebreaker key attribute */
		if (fc_key->keysz == fc_uppnkeyatts)
			return fc_key->scantid == NULL && fc_childheaptid != NULL;

		return fc_key->keysz < fc_uppnkeyatts;
	}

	return fc_cmp < 0;
}

/*
 * 给定 B-树页面的块编号，返回在 palloc() 分配的内存中的页面。
 * 同时对页面进行一些基本检查。
 *
 * 从未尝试通过多个并发缓冲区锁获取多个页面的一致视图；一般而言，我们一次只获取一个引脚和缓冲区锁，这通常是 nbtree 代码所需的全部。
 * （实际上，bt_recheck_sibling_links 会配对缓冲区锁，这是对这一通用规则的唯一例外。）
 *
 * 在页面副本上操作是有用的，因为它可以防止在基础操作类出现异常行为时控制权陷入不可中断状态。
 */
static Page fc_palloc_btree_page(BtreeCheckState *fc_state, BlockNumber fc_blocknum)
{
	Buffer		fc_buffer;
	Page		fc_page;
	BTPageOpaque fc_opaque;
	OffsetNumber fc_maxoffset;

	fc_page = palloc(BLCKSZ);

	/*
	 * 我们将页面复制到本地存储中，以避免在缓冲区上持有引脚的时间超过必要。
	 */
	fc_buffer = ReadBufferExtended(fc_state->rel, MAIN_FORKNUM, fc_blocknum, RBM_NORMAL,
								fc_state->checkstrategy);
	LockBuffer(fc_buffer, BT_READ);

	/*
	 * 执行 nbtree 本身对每个页面进行的相同基本合理性检查：
	 */
	_bt_checkpage(fc_state->rel, fc_buffer);

	/* 仅使用 palloc() 分配内存中的页面副本 */
	memcpy(fc_page, BufferGetPage(fc_buffer), BLCKSZ);
	UnlockReleaseBuffer(fc_buffer);

	fc_opaque = BTPageGetOpaque(fc_page);

	if (P_ISMETA(fc_opaque) && fc_blocknum != BTREE_METAPAGE)
		ereport(ERROR,
				(errcode(ERRCODE_INDEX_CORRUPTED),
				 errmsg("invalid meta page found at block %u in index \"%s\"",
						fc_blocknum, RelationGetRelationName(fc_state->rel))));

	/* 检查应为元页面的块中的页面 */
	if (fc_blocknum == BTREE_METAPAGE)
	{
		BTMetaPageData *fc_metad = BTPageGetMeta(fc_page);

		if (!P_ISMETA(fc_opaque) ||
			fc_metad->btm_magic != BTREE_MAGIC)
			ereport(ERROR,
					(errcode(ERRCODE_INDEX_CORRUPTED),
					 errmsg("index \"%s\" meta page is corrupt",
							RelationGetRelationName(fc_state->rel))));

		if (fc_metad->btm_version < BTREE_MIN_VERSION ||
			fc_metad->btm_version > BTREE_VERSION)
			ereport(ERROR,
					(errcode(ERRCODE_INDEX_CORRUPTED),
					 errmsg("version mismatch in index \"%s\": file version %d, "
							"current version %d, minimum supported version %d",
							RelationGetRelationName(fc_state->rel),
							fc_metad->btm_version, BTREE_VERSION,
							BTREE_MIN_VERSION)));

		/* 完成元页检查 */
		return fc_page;
	}

	/*
	 * 被删除的页面仍使用旧的32位XID表示，因其类型强制转换字段而没有合理的“级别”字段，
	 * 但其他所有页面（包括在Postgres 14+上删除的页面）都有有效的值。
	 */
	if (!P_ISDELETED(fc_opaque) || P_HAS_FULLXID(fc_opaque))
	{
		/* 好吧，没有理由不信任页面中的btpo_level字段 */

		if (P_ISLEAF(fc_opaque) && fc_opaque->btpo_level != 0)
			ereport(ERROR,
					(errcode(ERRCODE_INDEX_CORRUPTED),
					 errmsg_internal("invalid leaf page level %u for block %u in index \"%s\"",
									 fc_opaque->btpo_level, fc_blocknum,
									 RelationGetRelationName(fc_state->rel))));

		if (!P_ISLEAF(fc_opaque) && fc_opaque->btpo_level == 0)
			ereport(ERROR,
					(errcode(ERRCODE_INDEX_CORRUPTED),
					 errmsg_internal("invalid internal page level 0 for block %u in index \"%s\"",
									 fc_blocknum,
									 RelationGetRelationName(fc_state->rel))));
	}

	/*
	 * 页面项目数量的合理性检查。
	 *
	 * 如在_bt_binsrch()的开头所述，内部页面必须有子项，因为
	 * 必须始终有负无穷下行链接（也可能有高键）。 在非最右叶子
	 * 页面情况下，必须至少有一个高键。 例外是被删除的
	 * 页面，里面没有项目。
	 *
	 * 当页面半死时这是正确的，因为内部页面从不半死，
	 * 并且在半死时叶子页面必须有一个高键（最右的页面永远
	 * 不会被删除）。 在完全删除的页面中也是正确的：
	 * _bt_unlink_halfdead_page()不会更改目标页面的任何内容，
	 * 除了将页面标记为完全死亡，以及设置其xact字段。
	 * 特别是，它没有更改删除目标本身的兄弟链接，
	 * 因为当索引扫描落在删除目标上时是必需的，
	 * 然后需要向右移动（或者在向后索引扫描情况下
	 * 需要向左移动）。
	 */
	fc_maxoffset = PageGetMaxOffsetNumber(fc_page);
	if (fc_maxoffset > MaxIndexTuplesPerPage)
		ereport(ERROR,
				(errcode(ERRCODE_INDEX_CORRUPTED),
				 errmsg("Number of items on block %u of index \"%s\" exceeds MaxIndexTuplesPerPage (%u)",
						fc_blocknum, RelationGetRelationName(fc_state->rel),
						MaxIndexTuplesPerPage)));

	if (!P_ISLEAF(fc_opaque) && !P_ISDELETED(fc_opaque) && fc_maxoffset < P_FIRSTDATAKEY(fc_opaque))
		ereport(ERROR,
				(errcode(ERRCODE_INDEX_CORRUPTED),
				 errmsg("internal block %u in index \"%s\" lacks high key and/or at least one downlink",
						fc_blocknum, RelationGetRelationName(fc_state->rel))));

	if (P_ISLEAF(fc_opaque) && !P_ISDELETED(fc_opaque) && !P_RIGHTMOST(fc_opaque) && fc_maxoffset < P_HIKEY)
		ereport(ERROR,
				(errcode(ERRCODE_INDEX_CORRUPTED),
				 errmsg("non-rightmost leaf block %u in index \"%s\" lacks high key item",
						fc_blocknum, RelationGetRelationName(fc_state->rel))));

	/*
	 * 一般而言，内部页面从不标记为半死，除非在9.4及之前的
	 * Postgres版本中，这可以是有效的临时状态。
	 * 从9.4版本开始，该状态仍被VACUUM视为损坏，
	 * 所以在这里也这样做。请参阅_bt_pagedel()获取完整
	 * 的详细信息。
	 */
	if (!P_ISLEAF(fc_opaque) && P_ISHALFDEAD(fc_opaque))
		ereport(ERROR,
				(errcode(ERRCODE_INDEX_CORRUPTED),
				 errmsg("internal page block %u in index \"%s\" is half-dead",
						fc_blocknum, RelationGetRelationName(fc_state->rel)),
				 errhint("This can be caused by an interrupted VACUUM in version 9.3 or older, before upgrade. Please REINDEX it.")));

	/*
	 * 检查内部页面没有垃圾条目，并且没有页面具有
	 * 无效的与删除相关的页面级别标志组合
	 */
	if (!P_ISLEAF(fc_opaque) && P_HAS_GARBAGE(fc_opaque))
		ereport(ERROR,
				(errcode(ERRCODE_INDEX_CORRUPTED),
				 errmsg_internal("internal page block %u in index \"%s\" has garbage items",
								 fc_blocknum, RelationGetRelationName(fc_state->rel))));

	if (P_HAS_FULLXID(fc_opaque) && !P_ISDELETED(fc_opaque))
		ereport(ERROR,
				(errcode(ERRCODE_INDEX_CORRUPTED),
				 errmsg_internal("full transaction id page flag appears in non-deleted block %u in index \"%s\"",
								 fc_blocknum, RelationGetRelationName(fc_state->rel))));

	if (P_ISDELETED(fc_opaque) && P_ISHALFDEAD(fc_opaque))
		ereport(ERROR,
				(errcode(ERRCODE_INDEX_CORRUPTED),
				 errmsg_internal("deleted page block %u in index \"%s\" is half-dead",
								 fc_blocknum, RelationGetRelationName(fc_state->rel))));

	return fc_page;
}

/*
 * _bt_mkscankey()包装函数，自动防止插入的scankey被
 * 视为大于其值源自的基准元组（或某些其他相同的基准元组），
 * 在常见情况中，存在被截断/负无穷的属性。
 * 如果没有这一步，存在理论上amcheck可能无法报告的
 * 某些损坏形式。
 *
 * 例如，如果从目标的右兄弟页面上的第一个项目构建的
 * scankey恰好等于（而不是大于）目标页面上的最后一个
 * 项目，invariant_g_offset()可能会错过内部级别的
 * 跨页面不变性失败。 否则，_bt_compare()中的！pivotsearch
 * 决胜者可能导致amcheck假设（而不是实际验证）scankey
 * 更大。
 */
static inline BTScanInsert
fc_bt_mkscankey_pivotsearch(Relation fc_rel, IndexTuple fc_itup)
{
	BTScanInsert fc_skey;

	fc_skey = _bt_mkscankey(fc_rel, fc_itup);
	fc_skey->pivotsearch = true;

	return fc_skey;
}

/*
 * PageGetItemId() 包装器，用于验证返回的行指针。
 *
 * 缓冲区页/页项访问宏通常信任行指针没有损坏，这可能会导致验证本身出现问题。例如，PageGetItem() 并没有边界检查。传递给它一个损坏的行指针可能导致它返回一个不安全的元组/指针，无法解引用。
 *
 * 在元组之前验证行指针可以避免未定义行为和断言失败，尤其是在损坏索引的情况下，从而使验证过程更加稳健和可预测。
 */
static ItemId
fc_PageGetItemIdCareful(BtreeCheckState *fc_state, BlockNumber fc_block, Page fc_page,
					 OffsetNumber fc_offset)
{
	ItemId		fc_itemid = PageGetItemId(fc_page, fc_offset);

	if (ItemIdGetOffset(fc_itemid) + ItemIdGetLength(fc_itemid) >
		BLCKSZ - MAXALIGN(sizeof(BTPageOpaqueData)))
		ereport(ERROR,
				(errcode(ERRCODE_INDEX_CORRUPTED),
				 errmsg("line pointer points past end of tuple space in index \"%s\"",
						RelationGetRelationName(fc_state->rel)),
				 errdetail_internal("Index tid=(%u,%u) lp_off=%u, lp_len=%u lp_flags=%u.",
									fc_block, fc_offset, ItemIdGetOffset(fc_itemid),
									ItemIdGetLength(fc_itemid),
									ItemIdGetFlags(fc_itemid))));

	/*
	 * 验证行指针不是 LP_REDIRECT 或 LP_UNUSED，因为 nbtree 从不使用这两者。还要验证行指针是否具有存储，因为即使是 LP_DEAD 项在 nbtree 中也应该有存储。
	 */
	if (ItemIdIsRedirected(fc_itemid) || !ItemIdIsUsed(fc_itemid) ||
		ItemIdGetLength(fc_itemid) == 0)
		ereport(ERROR,
				(errcode(ERRCODE_INDEX_CORRUPTED),
				 errmsg("invalid line pointer storage in index \"%s\"",
						RelationGetRelationName(fc_state->rel)),
				 errdetail_internal("Index tid=(%u,%u) lp_off=%u, lp_len=%u lp_flags=%u.",
									fc_block, fc_offset, ItemIdGetOffset(fc_itemid),
									ItemIdGetLength(fc_itemid),
									ItemIdGetFlags(fc_itemid))));

	return fc_itemid;
}

/*
 * BTreeTupleGetHeapTID() 包装器，强制在必需的情况下（即对于非透视元组）存在堆 TID。
 */
static inline ItemPointer
fc_BTreeTupleGetHeapTIDCareful(BtreeCheckState *fc_state, IndexTuple fc_itup,
							bool fc_nonpivot)
{
	ItemPointer fc_htid;

	/*
	 * 调用者使用页面类型和项目偏移号来确定这是否应该是透视元组或非透视元组。验证元组元数据是否与此一致。
	 */
	Assert(fc_state->heapkeyspace);
	if (BTreeTupleIsPivot(fc_itup) && fc_nonpivot)
		ereport(ERROR,
				(errcode(ERRCODE_INDEX_CORRUPTED),
				 errmsg_internal("block %u or its right sibling block or child block in index \"%s\" has unexpected pivot tuple",
								 fc_state->targetblock,
								 RelationGetRelationName(fc_state->rel))));

	if (!BTreeTupleIsPivot(fc_itup) && !fc_nonpivot)
		ereport(ERROR,
				(errcode(ERRCODE_INDEX_CORRUPTED),
				 errmsg_internal("block %u or its right sibling block or child block in index \"%s\" has unexpected non-pivot tuple",
								 fc_state->targetblock,
								 RelationGetRelationName(fc_state->rel))));

	fc_htid = BTreeTupleGetHeapTID(fc_itup);
	if (!ItemPointerIsValid(fc_htid) && fc_nonpivot)
		ereport(ERROR,
				(errcode(ERRCODE_INDEX_CORRUPTED),
				 errmsg("block %u or its right sibling block or child block in index \"%s\" contains non-pivot tuple that lacks a heap TID",
						fc_state->targetblock,
						RelationGetRelationName(fc_state->rel))));

	return fc_htid;
}

/*
 * 返回 itup 的“指向” TID，用于生成描述性错误消息。itup 必须是“数据项”元组（因为在此调用高键元组没有多大意义，因无法显示有效的下联/块编号）。
 *
 * 返回的可以是一个堆 TID（如果 itup 是发布列表元组，则将是发布列表中的第一个堆 TID），或者包含下联块编号的 TID，以及一些编码的元数据（例如，itup 中存在的属性数量）。
 */
static inline ItemPointer
fc_BTreeTupleGetPointsToTID(IndexTuple fc_itup)
{
	/*
	 * 依赖于假设 !heapkeyspace 的内部页面数据项将在此处正确返回 TID 及下联 -- BTreeTupleGetHeapTID() 不会将其识别为透视元组，但一切仍然有效，因为 t_tid 字段仍会返回。
	 */
	if (!BTreeTupleIsPivot(fc_itup))
		return BTreeTupleGetHeapTID(fc_itup);

	/* 透视元组返回带下联块的 TID（堆空间变体） */
	return &fc_itup->t_tid;
}
